Vendor dependencies for GCS

This commit is contained in:
Alexander Neumann
2017-08-05 20:17:15 +02:00
parent ba75a3884c
commit 8ca6a9a240
1228 changed files with 1769186 additions and 1 deletions

View File

@@ -0,0 +1,450 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build linux
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"math/rand"
"os"
"sync"
"time"
"cloud.google.com/go/cmd/go-cloud-debug-agent/internal/breakpoints"
debuglet "cloud.google.com/go/cmd/go-cloud-debug-agent/internal/controller"
"cloud.google.com/go/cmd/go-cloud-debug-agent/internal/valuecollector"
"cloud.google.com/go/compute/metadata"
"golang.org/x/debug"
"golang.org/x/debug/local"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
cd "google.golang.org/api/clouddebugger/v2"
)
var (
appModule = flag.String("appmodule", "", "Optional application module name.")
appVersion = flag.String("appversion", "", "Optional application module version name.")
sourceContextFile = flag.String("sourcecontext", "", "File containing JSON-encoded source context.")
verbose = flag.Bool("v", false, "Output verbose log messages.")
projectNumber = flag.String("projectnumber", "", "Project number."+
" If this is not set, it is read from the GCP metadata server.")
projectID = flag.String("projectid", "", "Project ID."+
" If this is not set, it is read from the GCP metadata server.")
serviceAccountFile = flag.String("serviceaccountfile", "", "File containing JSON service account credentials.")
)
const (
maxCapturedStackFrames = 50
maxCapturedVariables = 1000
)
func main() {
flag.Usage = usage
flag.Parse()
args := flag.Args()
if len(args) == 0 {
// The user needs to supply the name of the executable to run.
flag.Usage()
return
}
if *projectNumber == "" {
var err error
*projectNumber, err = metadata.NumericProjectID()
if err != nil {
log.Print("Debuglet initialization: ", err)
}
}
if *projectID == "" {
var err error
*projectID, err = metadata.ProjectID()
if err != nil {
log.Print("Debuglet initialization: ", err)
}
}
sourceContexts, err := readSourceContextFile(*sourceContextFile)
if err != nil {
log.Print("Reading source context file: ", err)
}
var ts oauth2.TokenSource
ctx := context.Background()
if *serviceAccountFile != "" {
if ts, err = serviceAcctTokenSource(ctx, *serviceAccountFile, cd.CloudDebuggerScope); err != nil {
log.Fatalf("Error getting credentials from file %s: %v", *serviceAccountFile, err)
}
} else if ts, err = google.DefaultTokenSource(ctx, cd.CloudDebuggerScope); err != nil {
log.Print("Error getting application default credentials for Cloud Debugger:", err)
os.Exit(103)
}
c, err := debuglet.NewController(ctx, debuglet.Options{
ProjectNumber: *projectNumber,
ProjectID: *projectID,
AppModule: *appModule,
AppVersion: *appVersion,
SourceContexts: sourceContexts,
Verbose: *verbose,
TokenSource: ts,
})
if err != nil {
log.Fatal("Error connecting to Cloud Debugger: ", err)
}
prog, err := local.New(args[0])
if err != nil {
log.Fatal("Error loading program: ", err)
}
// Load the program, but don't actually start it running yet.
if _, err = prog.Run(args[1:]...); err != nil {
log.Fatal("Error loading program: ", err)
}
bs := breakpoints.NewBreakpointStore(prog)
// Seed the random number generator.
rand.Seed(time.Now().UnixNano())
// Now we want to do two things: run the user's program, and start sending
// List requests periodically to the Debuglet Controller to get breakpoints
// to set.
//
// We want to give the Debuglet Controller a chance to give us breakpoints
// before we start the program, otherwise we would miss any breakpoint
// triggers that occur during program startup -- for example, a breakpoint on
// the first line of main. But if the Debuglet Controller is not responding or
// is returning errors, we don't want to delay starting the program
// indefinitely.
//
// We pass a channel to breakpointListLoop, which will close it when the first
// List call finishes. Then we wait until either the channel is closed or a
// 5-second timer has finished before starting the program.
ch := make(chan bool)
// Start a goroutine that sends List requests to the Debuglet Controller, and
// sets any breakpoints it gets back.
go breakpointListLoop(ctx, c, bs, ch)
// Wait until 5 seconds have passed or breakpointListLoop has closed ch.
select {
case <-time.After(5 * time.Second):
case <-ch:
}
// Run the debuggee.
programLoop(ctx, c, bs, prog)
}
// usage prints a usage message to stderr and exits.
func usage() {
me := "a.out"
if len(os.Args) >= 1 {
me = os.Args[0]
}
fmt.Fprintf(os.Stderr, "Usage of %s:\n", me)
fmt.Fprintf(os.Stderr, "\t%s [flags...] -- <program name> args...\n", me)
fmt.Fprintf(os.Stderr, "Flags:\n")
flag.PrintDefaults()
fmt.Fprintf(os.Stderr,
"See https://cloud.google.com/tools/cloud-debugger/setting-up-on-compute-engine for more information.\n")
os.Exit(2)
}
// readSourceContextFile reads a JSON-encoded source context from the given file.
// It returns a non-empty slice on success.
func readSourceContextFile(filename string) ([]*cd.SourceContext, error) {
if filename == "" {
return nil, nil
}
scJSON, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("reading file %q: %v", filename, err)
}
var sc cd.SourceContext
if err = json.Unmarshal(scJSON, &sc); err != nil {
return nil, fmt.Errorf("parsing file %q: %v", filename, err)
}
return []*cd.SourceContext{&sc}, nil
}
// breakpointListLoop repeatedly calls the Debuglet Controller's List RPC, and
// passes the results to the BreakpointStore so it can set and unset breakpoints
// in the program.
//
// After the first List call finishes, ch is closed.
func breakpointListLoop(ctx context.Context, c *debuglet.Controller, bs *breakpoints.BreakpointStore, first chan bool) {
const (
avgTimeBetweenCalls = time.Second
errorDelay = 5 * time.Second
)
// randomDuration returns a random duration with expected value avg.
randomDuration := func(avg time.Duration) time.Duration {
return time.Duration(rand.Int63n(int64(2*avg + 1)))
}
var consecutiveFailures uint
for {
callStart := time.Now()
resp, err := c.List(ctx)
if err != nil && err != debuglet.ErrListUnchanged {
log.Printf("Debuglet controller server error: %v", err)
}
if err == nil {
bs.ProcessBreakpointList(resp.Breakpoints)
}
if first != nil {
// We've finished one call to List and set any breakpoints we received.
close(first)
first = nil
}
// Asynchronously send updates for any breakpoints that caused an error when
// the BreakpointStore tried to process them. We don't wait for the update
// to finish before the program can exit, as we do for normal updates.
errorBps := bs.ErrorBreakpoints()
for _, bp := range errorBps {
go func(bp *cd.Breakpoint) {
if err := c.Update(ctx, bp.Id, bp); err != nil {
log.Printf("Failed to send breakpoint update for %s: %s", bp.Id, err)
}
}(bp)
}
// Make the next call not too soon after the one we just did.
delay := randomDuration(avgTimeBetweenCalls)
// If the call returned an error other than ErrListUnchanged, wait longer.
if err != nil && err != debuglet.ErrListUnchanged {
// Wait twice as long after each consecutive failure, to a maximum of 16x.
delay += randomDuration(errorDelay * (1 << consecutiveFailures))
if consecutiveFailures < 4 {
consecutiveFailures++
}
} else {
consecutiveFailures = 0
}
// Sleep until we reach time callStart+delay. If we've already passed that
// time, time.Sleep will return immediately -- this should be the common
// case, since the server will delay responding to List for a while when
// there are no changes to report.
time.Sleep(callStart.Add(delay).Sub(time.Now()))
}
}
// programLoop runs the program being debugged to completion. When a breakpoint's
// conditions are satisfied, it sends an Update RPC to the Debuglet Controller.
// The function returns when the program exits and all Update RPCs have finished.
func programLoop(ctx context.Context, c *debuglet.Controller, bs *breakpoints.BreakpointStore, prog debug.Program) {
var wg sync.WaitGroup
for {
// Run the program until it hits a breakpoint or exits.
status, err := prog.Resume()
if err != nil {
break
}
// Get the breakpoints at this address whose conditions were satisfied,
// and remove the ones that aren't logpoints.
bps := bs.BreakpointsAtPC(status.PC)
bps = bpsWithConditionSatisfied(bps, prog)
for _, bp := range bps {
if bp.Action != "LOG" {
bs.RemoveBreakpoint(bp)
}
}
if len(bps) == 0 {
continue
}
// Evaluate expressions and get the stack.
vc := valuecollector.NewCollector(prog, maxCapturedVariables)
needStackFrames := false
for _, bp := range bps {
// If evaluating bp's condition didn't return an error, evaluate bp's
// expressions, and later get the stack frames.
if bp.Status == nil {
bp.EvaluatedExpressions = expressionValues(bp.Expressions, prog, vc)
needStackFrames = true
}
}
var (
stack []*cd.StackFrame
stackFramesStatusMessage *cd.StatusMessage
)
if needStackFrames {
stack, stackFramesStatusMessage = stackFrames(prog, vc)
}
// Read variable values from the program.
variableTable := vc.ReadValues()
// Start a goroutine to send updates to the Debuglet Controller or write
// to logs, concurrently with resuming the program.
// TODO: retry Update on failure.
for _, bp := range bps {
wg.Add(1)
switch bp.Action {
case "LOG":
go func(format string, evaluatedExpressions []*cd.Variable) {
s := valuecollector.LogString(format, evaluatedExpressions, variableTable)
log.Print(s)
wg.Done()
}(bp.LogMessageFormat, bp.EvaluatedExpressions)
bp.Status = nil
bp.EvaluatedExpressions = nil
default:
go func(bp *cd.Breakpoint) {
defer wg.Done()
bp.IsFinalState = true
if bp.Status == nil {
// If evaluating bp's condition didn't return an error, include the
// stack frames, variable table, and any status message produced when
// getting the stack frames.
bp.StackFrames = stack
bp.VariableTable = variableTable
bp.Status = stackFramesStatusMessage
}
if err := c.Update(ctx, bp.Id, bp); err != nil {
log.Printf("Failed to send breakpoint update for %s: %s", bp.Id, err)
}
}(bp)
}
}
}
// Wait for all updates to finish before returning.
wg.Wait()
}
// bpsWithConditionSatisfied returns the breakpoints whose conditions are true
// (or that do not have a condition.)
func bpsWithConditionSatisfied(bpsIn []*cd.Breakpoint, prog debug.Program) []*cd.Breakpoint {
var bpsOut []*cd.Breakpoint
for _, bp := range bpsIn {
cond, err := condTruth(bp.Condition, prog)
if err != nil {
bp.Status = errorStatusMessage(err.Error(), refersToBreakpointCondition)
// Include bp in the list to be updated when there's an error, so that
// the user gets a response.
bpsOut = append(bpsOut, bp)
} else if cond {
bpsOut = append(bpsOut, bp)
}
}
return bpsOut
}
// condTruth evaluates a condition.
func condTruth(condition string, prog debug.Program) (bool, error) {
if condition == "" {
// A condition wasn't set.
return true, nil
}
val, err := prog.Evaluate(condition)
if err != nil {
return false, err
}
if v, ok := val.(bool); !ok {
return false, fmt.Errorf("condition expression has type %T, should be bool", val)
} else {
return v, nil
}
}
// expressionValues evaluates a slice of expressions and returns a []*cd.Variable
// containing the results.
// If the result of an expression evaluation refers to values from the program's
// memory (e.g., the expression evaluates to a slice) a corresponding variable is
// added to the value collector, to be read later.
func expressionValues(expressions []string, prog debug.Program, vc *valuecollector.Collector) []*cd.Variable {
evaluatedExpressions := make([]*cd.Variable, len(expressions))
for i, exp := range expressions {
ee := &cd.Variable{Name: exp}
evaluatedExpressions[i] = ee
if val, err := prog.Evaluate(exp); err != nil {
ee.Status = errorStatusMessage(err.Error(), refersToBreakpointExpression)
} else {
vc.FillValue(val, ee)
}
}
return evaluatedExpressions
}
// stackFrames returns a stack trace for the program. It passes references to
// function parameters and local variables to the value collector, so it can read
// their values later.
func stackFrames(prog debug.Program, vc *valuecollector.Collector) ([]*cd.StackFrame, *cd.StatusMessage) {
frames, err := prog.Frames(maxCapturedStackFrames)
if err != nil {
return nil, errorStatusMessage("Error getting stack: "+err.Error(), refersToUnspecified)
}
stackFrames := make([]*cd.StackFrame, len(frames))
for i, f := range frames {
frame := &cd.StackFrame{}
frame.Function = f.Function
for _, v := range f.Params {
frame.Arguments = append(frame.Arguments, vc.AddVariable(debug.LocalVar(v)))
}
for _, v := range f.Vars {
frame.Locals = append(frame.Locals, vc.AddVariable(v))
}
frame.Location = &cd.SourceLocation{
Path: f.File,
Line: int64(f.Line),
}
stackFrames[i] = frame
}
return stackFrames, nil
}
// errorStatusMessage returns a *cd.StatusMessage indicating an error,
// with the given message and refersTo field.
func errorStatusMessage(msg string, refersTo int) *cd.StatusMessage {
return &cd.StatusMessage{
Description: &cd.FormatMessage{Format: "$0", Parameters: []string{msg}},
IsError: true,
RefersTo: refersToString[refersTo],
}
}
const (
// RefersTo values for cd.StatusMessage.
refersToUnspecified = iota
refersToBreakpointCondition
refersToBreakpointExpression
)
// refersToString contains the strings for each refersTo value.
// See the definition of StatusMessage in the v2/clouddebugger package.
var refersToString = map[int]string{
refersToUnspecified: "UNSPECIFIED",
refersToBreakpointCondition: "BREAKPOINT_CONDITION",
refersToBreakpointExpression: "BREAKPOINT_EXPRESSION",
}
func serviceAcctTokenSource(ctx context.Context, filename string, scope ...string) (oauth2.TokenSource, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("cannot read service account file: %v", err)
}
cfg, err := google.JWTConfigFromJSON(data, scope...)
if err != nil {
return nil, fmt.Errorf("google.JWTConfigFromJSON: %v", err)
}
return cfg.TokenSource(ctx), nil
}

View File

@@ -0,0 +1,174 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package breakpoints handles breakpoint requests we get from the user through
// the Debuglet Controller, and manages corresponding breakpoints set in the code.
package breakpoints
import (
"log"
"sync"
"golang.org/x/debug"
cd "google.golang.org/api/clouddebugger/v2"
)
// BreakpointStore stores the set of breakpoints for a program.
type BreakpointStore struct {
mu sync.Mutex
// prog is the program being debugged.
prog debug.Program
// idToBreakpoint is a map from breakpoint identifier to *cd.Breakpoint. The
// map value is nil if the breakpoint is inactive. A breakpoint is active if:
// - We received it from the Debuglet Controller, and it was active at the time;
// - We were able to set code breakpoints for it;
// - We have not reached any of those code breakpoints while satisfying the
// breakpoint's conditions, or the breakpoint has action LOG; and
// - The Debuglet Controller hasn't informed us the breakpoint has become inactive.
idToBreakpoint map[string]*cd.Breakpoint
// pcToBps and bpToPCs store the many-to-many relationship between breakpoints we
// received from the Debuglet Controller and the code breakpoints we set for them.
pcToBps map[uint64][]*cd.Breakpoint
bpToPCs map[*cd.Breakpoint][]uint64
// errors contains any breakpoints which couldn't be set because they caused an
// error. These are retrieved with ErrorBreakpoints, and the caller is
// expected to handle sending updates for them.
errors []*cd.Breakpoint
}
// NewBreakpointStore returns a BreakpointStore for the given program.
func NewBreakpointStore(prog debug.Program) *BreakpointStore {
return &BreakpointStore{
idToBreakpoint: make(map[string]*cd.Breakpoint),
pcToBps: make(map[uint64][]*cd.Breakpoint),
bpToPCs: make(map[*cd.Breakpoint][]uint64),
prog: prog,
}
}
// ProcessBreakpointList applies updates received from the Debuglet Controller through a List call.
func (bs *BreakpointStore) ProcessBreakpointList(bps []*cd.Breakpoint) {
bs.mu.Lock()
defer bs.mu.Unlock()
for _, bp := range bps {
if storedBp, ok := bs.idToBreakpoint[bp.Id]; ok {
if storedBp != nil && bp.IsFinalState {
// IsFinalState indicates that the breakpoint has been made inactive.
bs.removeBreakpointLocked(storedBp)
}
} else {
if bp.IsFinalState {
// The controller is notifying us that the breakpoint is no longer active,
// but we didn't know about it anyway.
continue
}
if bp.Action != "" && bp.Action != "CAPTURE" && bp.Action != "LOG" {
bp.IsFinalState = true
bp.Status = &cd.StatusMessage{
Description: &cd.FormatMessage{Format: "Action is not supported"},
IsError: true,
}
bs.errors = append(bs.errors, bp)
// Note in idToBreakpoint that we've already seen this breakpoint, so that we
// don't try to report it as an error multiple times.
bs.idToBreakpoint[bp.Id] = nil
continue
}
pcs, err := bs.prog.BreakpointAtLine(bp.Location.Path, uint64(bp.Location.Line))
if err != nil {
log.Printf("error setting breakpoint at %s:%d: %v", bp.Location.Path, bp.Location.Line, err)
}
if len(pcs) == 0 {
// We can't find a PC for this breakpoint's source line, so don't make it active.
// TODO: we could snap the line to a location where we can break, or report an error to the user.
bs.idToBreakpoint[bp.Id] = nil
} else {
bs.idToBreakpoint[bp.Id] = bp
for _, pc := range pcs {
bs.pcToBps[pc] = append(bs.pcToBps[pc], bp)
}
bs.bpToPCs[bp] = pcs
}
}
}
}
// ErrorBreakpoints returns a slice of Breakpoints that caused errors when the
// BreakpointStore tried to process them, and resets the list of such
// breakpoints.
// The caller is expected to send updates to the server to indicate the errors.
func (bs *BreakpointStore) ErrorBreakpoints() []*cd.Breakpoint {
bs.mu.Lock()
defer bs.mu.Unlock()
bps := bs.errors
bs.errors = nil
return bps
}
// BreakpointsAtPC returns all the breakpoints for which we set a code
// breakpoint at the given address.
func (bs *BreakpointStore) BreakpointsAtPC(pc uint64) []*cd.Breakpoint {
bs.mu.Lock()
defer bs.mu.Unlock()
return bs.pcToBps[pc]
}
// RemoveBreakpoint makes the given breakpoint inactive.
// This is called when either the debugged program hits the breakpoint, or the Debuglet
// Controller informs us that the breakpoint is now inactive.
func (bs *BreakpointStore) RemoveBreakpoint(bp *cd.Breakpoint) {
bs.mu.Lock()
bs.removeBreakpointLocked(bp)
bs.mu.Unlock()
}
func (bs *BreakpointStore) removeBreakpointLocked(bp *cd.Breakpoint) {
// Set the ID's corresponding breakpoint to nil, so that we won't activate it
// if we see it again.
// TODO: we could delete it after a few seconds.
bs.idToBreakpoint[bp.Id] = nil
// Delete bp from the list of cd breakpoints at each of its corresponding
// code breakpoint locations, and delete any code breakpoints which no longer
// have a corresponding cd breakpoint.
var codeBreakpointsToDelete []uint64
for _, pc := range bs.bpToPCs[bp] {
bps := remove(bs.pcToBps[pc], bp)
if len(bps) == 0 {
// bp was the last breakpoint set at this PC, so delete the code breakpoint.
codeBreakpointsToDelete = append(codeBreakpointsToDelete, pc)
delete(bs.pcToBps, pc)
} else {
bs.pcToBps[pc] = bps
}
}
if len(codeBreakpointsToDelete) > 0 {
bs.prog.DeleteBreakpoints(codeBreakpointsToDelete)
}
delete(bs.bpToPCs, bp)
}
// remove updates rs by removing r, then returns rs.
// The mutex in the BreakpointStore which contains rs should be held.
func remove(rs []*cd.Breakpoint, r *cd.Breakpoint) []*cd.Breakpoint {
for i := range rs {
if rs[i] == r {
rs[i] = rs[len(rs)-1]
rs = rs[0 : len(rs)-1]
return rs
}
}
// We shouldn't reach here.
return rs
}

View File

@@ -0,0 +1,168 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package breakpoints
import (
"reflect"
"testing"
"golang.org/x/debug"
cd "google.golang.org/api/clouddebugger/v2"
)
var (
testPC1 uint64 = 0x1234
testPC2 uint64 = 0x5678
testPC3 uint64 = 0x3333
testFile = "foo.go"
testLine uint64 = 42
testLine2 uint64 = 99
testLogPC uint64 = 0x9abc
testLogLine uint64 = 43
testBadPC uint64 = 0xdef0
testBadLine uint64 = 44
testBP = &cd.Breakpoint{
Action: "CAPTURE",
Id: "TestBreakpoint",
IsFinalState: false,
Location: &cd.SourceLocation{Path: testFile, Line: int64(testLine)},
}
testBP2 = &cd.Breakpoint{
Action: "CAPTURE",
Id: "TestBreakpoint2",
IsFinalState: false,
Location: &cd.SourceLocation{Path: testFile, Line: int64(testLine2)},
}
testLogBP = &cd.Breakpoint{
Action: "LOG",
Id: "TestLogBreakpoint",
IsFinalState: false,
Location: &cd.SourceLocation{Path: testFile, Line: int64(testLogLine)},
}
testBadBP = &cd.Breakpoint{
Action: "BEEP",
Id: "TestBadBreakpoint",
IsFinalState: false,
Location: &cd.SourceLocation{Path: testFile, Line: int64(testBadLine)},
}
)
func TestBreakpointStore(t *testing.T) {
p := &Program{breakpointPCs: make(map[uint64]bool)}
bs := NewBreakpointStore(p)
checkPCs := func(expected map[uint64]bool) {
if !reflect.DeepEqual(p.breakpointPCs, expected) {
t.Errorf("got breakpoint map %v want %v", p.breakpointPCs, expected)
}
}
bs.ProcessBreakpointList([]*cd.Breakpoint{testBP, testBP2, testLogBP, testBadBP})
checkPCs(map[uint64]bool{
testPC1: true,
testPC2: true,
testPC3: true,
testLogPC: true,
})
for _, test := range []struct {
pc uint64
expected []*cd.Breakpoint
}{
{testPC1, []*cd.Breakpoint{testBP}},
{testPC2, []*cd.Breakpoint{testBP}},
{testPC3, []*cd.Breakpoint{testBP2}},
{testLogPC, []*cd.Breakpoint{testLogBP}},
} {
if bps := bs.BreakpointsAtPC(test.pc); !reflect.DeepEqual(bps, test.expected) {
t.Errorf("BreakpointsAtPC(%x): got %v want %v", test.pc, bps, test.expected)
}
}
testBP2.IsFinalState = true
bs.ProcessBreakpointList([]*cd.Breakpoint{testBP, testBP2, testLogBP, testBadBP})
checkPCs(map[uint64]bool{
testPC1: true,
testPC2: true,
testPC3: false,
testLogPC: true,
})
bs.RemoveBreakpoint(testBP)
checkPCs(map[uint64]bool{
testPC1: false,
testPC2: false,
testPC3: false,
testLogPC: true,
})
for _, pc := range []uint64{testPC1, testPC2, testPC3} {
if bps := bs.BreakpointsAtPC(pc); len(bps) != 0 {
t.Errorf("BreakpointsAtPC(%x): got %v want []", pc, bps)
}
}
// bs.ErrorBreakpoints should return testBadBP.
errorBps := bs.ErrorBreakpoints()
if len(errorBps) != 1 {
t.Errorf("ErrorBreakpoints: got %d want 1", len(errorBps))
} else {
bp := errorBps[0]
if bp.Id != testBadBP.Id {
t.Errorf("ErrorBreakpoints: got id %q want 1", bp.Id)
}
if bp.Status == nil || !bp.Status.IsError {
t.Errorf("ErrorBreakpoints: got %v, want error", bp.Status)
}
}
// The error should have been removed by the last call to bs.ErrorBreakpoints.
errorBps = bs.ErrorBreakpoints()
if len(errorBps) != 0 {
t.Errorf("ErrorBreakpoints: got %d want 0", len(errorBps))
}
// Even if testBadBP is sent in a new list, it should not be returned again.
bs.ProcessBreakpointList([]*cd.Breakpoint{testBadBP})
errorBps = bs.ErrorBreakpoints()
if len(errorBps) != 0 {
t.Errorf("ErrorBreakpoints: got %d want 0", len(errorBps))
}
}
// Program implements the similarly-named interface in x/debug.
// ValueCollector should only call its BreakpointAtLine and DeleteBreakpoints methods.
type Program struct {
debug.Program
// breakpointPCs contains the state of code breakpoints -- true if the
// breakpoint is currently set, false if it has been deleted.
breakpointPCs map[uint64]bool
}
func (p *Program) BreakpointAtLine(file string, line uint64) ([]uint64, error) {
var pcs []uint64
switch {
case file == testFile && line == testLine:
pcs = []uint64{testPC1, testPC2}
case file == testFile && line == testLine2:
pcs = []uint64{testPC3}
case file == testFile && line == testLogLine:
pcs = []uint64{testLogPC}
default:
pcs = []uint64{0xbad}
}
for _, pc := range pcs {
p.breakpointPCs[pc] = true
}
return pcs, nil
}
func (p *Program) DeleteBreakpoints(pcs []uint64) error {
for _, pc := range pcs {
p.breakpointPCs[pc] = false
}
return nil
}

View File

@@ -0,0 +1,291 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package controller is a library for interacting with the Google Cloud Debugger's Debuglet Controller service.
package controller
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"log"
"sync"
"golang.org/x/net/context"
"golang.org/x/oauth2"
cd "google.golang.org/api/clouddebugger/v2"
"google.golang.org/api/googleapi"
"google.golang.org/api/option"
htransport "google.golang.org/api/transport/http"
)
const (
// agentVersionString identifies the agent to the service.
agentVersionString = "google.com/go-gcp/v0.2"
// initWaitToken is the wait token sent in the first Update request to a server.
initWaitToken = "init"
)
var (
// ErrListUnchanged is returned by List if the server time limit is reached
// before the list of breakpoints changes.
ErrListUnchanged = errors.New("breakpoint list unchanged")
// ErrDebuggeeDisabled is returned by List or Update if the server has disabled
// this Debuggee. The caller can retry later.
ErrDebuggeeDisabled = errors.New("debuglet disabled by server")
)
// Controller manages a connection to the Debuglet Controller service.
type Controller struct {
s serviceInterface
// waitToken is sent with List requests so the server knows which set of
// breakpoints this client has already seen. Each successful List request
// returns a new waitToken to send in the next request.
waitToken string
// verbose determines whether to do some logging
verbose bool
// options, uniquifier and description are used in register.
options Options
uniquifier string
description string
// labels are included when registering the debuggee. They should contain
// the module name, version and minorversion, and are used by the debug UI
// to label the correct version active for debugging.
labels map[string]string
// mu protects debuggeeID
mu sync.Mutex
// debuggeeID is returned from the server on registration, and is passed back
// to the server in List and Update requests.
debuggeeID string
}
// Options controls how the Debuglet Controller client identifies itself to the server.
// See https://cloud.google.com/storage/docs/projects and
// https://cloud.google.com/tools/cloud-debugger/setting-up-on-compute-engine
// for further documentation of these parameters.
type Options struct {
ProjectNumber string // GCP Project Number.
ProjectID string // GCP Project ID.
AppModule string // Module name for the debugged program.
AppVersion string // Version number for this module.
SourceContexts []*cd.SourceContext // Description of source.
Verbose bool
TokenSource oauth2.TokenSource // Source of Credentials used for Stackdriver Debugger.
}
type serviceInterface interface {
Register(ctx context.Context, req *cd.RegisterDebuggeeRequest) (*cd.RegisterDebuggeeResponse, error)
Update(ctx context.Context, debuggeeID, breakpointID string, req *cd.UpdateActiveBreakpointRequest) (*cd.UpdateActiveBreakpointResponse, error)
List(ctx context.Context, debuggeeID, waitToken string) (*cd.ListActiveBreakpointsResponse, error)
}
var newService = func(ctx context.Context, tokenSource oauth2.TokenSource) (serviceInterface, error) {
httpClient, endpoint, err := htransport.NewClient(ctx, option.WithTokenSource(tokenSource))
if err != nil {
return nil, err
}
s, err := cd.New(httpClient)
if err != nil {
return nil, err
}
if endpoint != "" {
s.BasePath = endpoint
}
return &service{s: s}, nil
}
type service struct {
s *cd.Service
}
func (s service) Register(ctx context.Context, req *cd.RegisterDebuggeeRequest) (*cd.RegisterDebuggeeResponse, error) {
call := cd.NewControllerDebuggeesService(s.s).Register(req)
return call.Context(ctx).Do()
}
func (s service) Update(ctx context.Context, debuggeeID, breakpointID string, req *cd.UpdateActiveBreakpointRequest) (*cd.UpdateActiveBreakpointResponse, error) {
call := cd.NewControllerDebuggeesBreakpointsService(s.s).Update(debuggeeID, breakpointID, req)
return call.Context(ctx).Do()
}
func (s service) List(ctx context.Context, debuggeeID, waitToken string) (*cd.ListActiveBreakpointsResponse, error) {
call := cd.NewControllerDebuggeesBreakpointsService(s.s).List(debuggeeID)
call.WaitToken(waitToken)
return call.Context(ctx).Do()
}
// NewController connects to the Debuglet Controller server using the given options,
// and returns a Controller for that connection.
// Google Application Default Credentials are used to connect to the Debuglet Controller;
// see https://developers.google.com/identity/protocols/application-default-credentials
func NewController(ctx context.Context, o Options) (*Controller, error) {
// We build a JSON encoding of o.SourceContexts so we can hash it.
scJSON, err := json.Marshal(o.SourceContexts)
if err != nil {
scJSON = nil
o.SourceContexts = nil
}
const minorversion = "107157" // any arbitrary numeric string
// Compute a uniquifier string by hashing the project number, app module name,
// app module version, debuglet version, and source context.
// The choice of hash function is arbitrary.
h := sha256.Sum256([]byte(fmt.Sprintf("%d %s %d %s %d %s %d %s %d %s %d %s",
len(o.ProjectNumber), o.ProjectNumber,
len(o.AppModule), o.AppModule,
len(o.AppVersion), o.AppVersion,
len(agentVersionString), agentVersionString,
len(scJSON), scJSON,
len(minorversion), minorversion)))
uniquifier := fmt.Sprintf("%X", h[0:16]) // 32 hex characters
description := o.ProjectID
if o.AppModule != "" {
description += "-" + o.AppModule
}
if o.AppVersion != "" {
description += "-" + o.AppVersion
}
s, err := newService(ctx, o.TokenSource)
if err != nil {
return nil, err
}
// Construct client.
c := &Controller{
s: s,
waitToken: initWaitToken,
verbose: o.Verbose,
options: o,
uniquifier: uniquifier,
description: description,
labels: map[string]string{
"module": o.AppModule,
"version": o.AppVersion,
"minorversion": minorversion,
},
}
return c, nil
}
func (c *Controller) getDebuggeeID(ctx context.Context) (string, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.debuggeeID != "" {
return c.debuggeeID, nil
}
// The debuglet hasn't been registered yet, or it is disabled and we should try registering again.
if err := c.register(ctx); err != nil {
return "", err
}
return c.debuggeeID, nil
}
// List retrieves the current list of breakpoints from the server.
// If the set of breakpoints on the server is the same as the one returned in
// the previous call to List, the server can delay responding until it changes,
// and return an error instead if no change occurs before a time limit the
// server sets. List can't be called concurrently with itself.
func (c *Controller) List(ctx context.Context) (*cd.ListActiveBreakpointsResponse, error) {
id, err := c.getDebuggeeID(ctx)
if err != nil {
return nil, err
}
resp, err := c.s.List(ctx, id, c.waitToken)
if err != nil {
if isAbortedError(err) {
return nil, ErrListUnchanged
}
// For other errors, the protocol requires that we attempt to re-register.
c.mu.Lock()
defer c.mu.Unlock()
if regError := c.register(ctx); regError != nil {
return nil, regError
}
return nil, err
}
if resp == nil {
return nil, errors.New("no response")
}
if c.verbose {
log.Printf("List response: %v", resp)
}
c.waitToken = resp.NextWaitToken
return resp, nil
}
// isAbortedError tests if err is a *googleapi.Error, that it contains one error
// in Errors, and that that error's Reason is "aborted".
func isAbortedError(err error) bool {
e, _ := err.(*googleapi.Error)
if e == nil {
return false
}
if len(e.Errors) != 1 {
return false
}
return e.Errors[0].Reason == "aborted"
}
// Update reports information to the server about a breakpoint that was hit.
// Update can be called concurrently with List and Update.
func (c *Controller) Update(ctx context.Context, breakpointID string, bp *cd.Breakpoint) error {
req := &cd.UpdateActiveBreakpointRequest{Breakpoint: bp}
if c.verbose {
log.Printf("sending update for %s: %v", breakpointID, req)
}
id, err := c.getDebuggeeID(ctx)
if err != nil {
return err
}
_, err = c.s.Update(ctx, id, breakpointID, req)
return err
}
// register calls the Debuglet Controller Register method, and sets c.debuggeeID.
// c.mu should be locked while calling this function. List and Update can't
// make progress until it returns.
func (c *Controller) register(ctx context.Context) error {
req := cd.RegisterDebuggeeRequest{
Debuggee: &cd.Debuggee{
AgentVersion: agentVersionString,
Description: c.description,
Project: c.options.ProjectNumber,
SourceContexts: c.options.SourceContexts,
Uniquifier: c.uniquifier,
Labels: c.labels,
},
}
resp, err := c.s.Register(ctx, &req)
if err != nil {
return err
}
if resp == nil {
return errors.New("register: no response")
}
if resp.Debuggee.IsDisabled {
// Setting c.debuggeeID to empty makes sure future List and Update calls
// will call register first.
c.debuggeeID = ""
} else {
c.debuggeeID = resp.Debuggee.Id
}
if c.debuggeeID == "" {
return ErrDebuggeeDisabled
}
return nil
}

View File

@@ -0,0 +1,254 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controller
import (
"bytes"
"errors"
"fmt"
"strconv"
"testing"
"golang.org/x/net/context"
"golang.org/x/oauth2"
cd "google.golang.org/api/clouddebugger/v2"
"google.golang.org/api/googleapi"
)
const (
testDebuggeeID = "d12345"
testBreakpointID = "bp12345"
)
var (
// The sequence of wait tokens in List requests and responses.
expectedWaitToken = []string{"init", "token1", "token2", "token1", "token1"}
// The set of breakpoints returned from each List call.
expectedBreakpoints = [][]*cd.Breakpoint{
nil,
{
&cd.Breakpoint{
Id: testBreakpointID,
IsFinalState: false,
Location: &cd.SourceLocation{Line: 42, Path: "foo.go"},
},
},
nil,
}
abortedError error = &googleapi.Error{
Code: 409,
Message: "Conflict",
Body: `{
"error": {
"errors": [
{
"domain": "global",
"reason": "aborted",
"message": "Conflict"
}
],
"code": 409,
"message": "Conflict"
}
}`,
Errors: []googleapi.ErrorItem{
{Reason: "aborted", Message: "Conflict"},
},
}
backendError error = &googleapi.Error{
Code: 503,
Message: "Backend Error",
Body: `{
"error": {
"errors": [
{
"domain": "global",
"reason": "backendError",
"message": "Backend Error"
}
],
"code": 503,
"message": "Backend Error"
}
}`,
Errors: []googleapi.ErrorItem{
{Reason: "backendError", Message: "Backend Error"},
},
}
)
type mockService struct {
t *testing.T
listCallsSeen int
registerCallsSeen int
}
func (s *mockService) Register(ctx context.Context, req *cd.RegisterDebuggeeRequest) (*cd.RegisterDebuggeeResponse, error) {
s.registerCallsSeen++
if req.Debuggee == nil {
s.t.Errorf("missing debuggee")
return nil, nil
}
if req.Debuggee.AgentVersion == "" {
s.t.Errorf("missing agent version")
}
if req.Debuggee.Description == "" {
s.t.Errorf("missing debuglet description")
}
if req.Debuggee.Project == "" {
s.t.Errorf("missing project id")
}
if req.Debuggee.Uniquifier == "" {
s.t.Errorf("missing uniquifier")
}
return &cd.RegisterDebuggeeResponse{
Debuggee: &cd.Debuggee{Id: testDebuggeeID},
}, nil
}
func (s *mockService) Update(ctx context.Context, id, breakpointID string, req *cd.UpdateActiveBreakpointRequest) (*cd.UpdateActiveBreakpointResponse, error) {
if id != testDebuggeeID {
s.t.Errorf("got debuggee ID %s want %s", id, testDebuggeeID)
}
if breakpointID != testBreakpointID {
s.t.Errorf("got breakpoint ID %s want %s", breakpointID, testBreakpointID)
}
if !req.Breakpoint.IsFinalState {
s.t.Errorf("got IsFinalState = false, want true")
}
return nil, nil
}
func (s *mockService) List(ctx context.Context, id, waitToken string) (*cd.ListActiveBreakpointsResponse, error) {
if id != testDebuggeeID {
s.t.Errorf("got debuggee ID %s want %s", id, testDebuggeeID)
}
if waitToken != expectedWaitToken[s.listCallsSeen] {
s.t.Errorf("got wait token %s want %s", waitToken, expectedWaitToken[s.listCallsSeen])
}
s.listCallsSeen++
if s.listCallsSeen == 4 {
return nil, backendError
}
if s.listCallsSeen == 5 {
return nil, abortedError
}
resp := &cd.ListActiveBreakpointsResponse{
Breakpoints: expectedBreakpoints[s.listCallsSeen-1],
NextWaitToken: expectedWaitToken[s.listCallsSeen],
}
return resp, nil
}
func TestDebugletControllerClientLibrary(t *testing.T) {
var (
m *mockService
c *Controller
list *cd.ListActiveBreakpointsResponse
err error
)
m = &mockService{t: t}
newService = func(context.Context, oauth2.TokenSource) (serviceInterface, error) { return m, nil }
opts := Options{
ProjectNumber: "5",
ProjectID: "p1",
AppModule: "mod1",
AppVersion: "v1",
}
ctx := context.Background()
if c, err = NewController(ctx, opts); err != nil {
t.Fatal("Initializing Controller client:", err)
}
if err := validateLabels(c, opts); err != nil {
t.Fatalf("Invalid labels:\n%v", err)
}
if list, err = c.List(ctx); err != nil {
t.Fatal("List:", err)
}
if m.registerCallsSeen != 1 {
t.Errorf("saw %d Register calls, want 1", m.registerCallsSeen)
}
if list, err = c.List(ctx); err != nil {
t.Fatal("List:", err)
}
if len(list.Breakpoints) != 1 {
t.Fatalf("got %d breakpoints, want 1", len(list.Breakpoints))
}
if err = c.Update(ctx, list.Breakpoints[0].Id, &cd.Breakpoint{Id: testBreakpointID, IsFinalState: true}); err != nil {
t.Fatal("Update:", err)
}
if list, err = c.List(ctx); err != nil {
t.Fatal("List:", err)
}
if m.registerCallsSeen != 1 {
t.Errorf("saw %d Register calls, want 1", m.registerCallsSeen)
}
// The next List call produces an error that should cause a Register call.
if list, err = c.List(ctx); err == nil {
t.Fatal("List should have returned an error")
}
if m.registerCallsSeen != 2 {
t.Errorf("saw %d Register calls, want 2", m.registerCallsSeen)
}
// The next List call produces an error that should not cause a Register call.
if list, err = c.List(ctx); err == nil {
t.Fatal("List should have returned an error")
}
if m.registerCallsSeen != 2 {
t.Errorf("saw %d Register calls, want 2", m.registerCallsSeen)
}
if m.listCallsSeen != 5 {
t.Errorf("saw %d list calls, want 5", m.listCallsSeen)
}
}
func validateLabels(c *Controller, o Options) error {
errMsg := new(bytes.Buffer)
if m, ok := c.labels["module"]; ok {
if m != o.AppModule {
errMsg.WriteString(fmt.Sprintf("label module: want %s, got %s\n", o.AppModule, m))
}
} else {
errMsg.WriteString("Missing \"module\" label\n")
}
if v, ok := c.labels["version"]; ok {
if v != o.AppVersion {
errMsg.WriteString(fmt.Sprintf("label version: want %s, got %s\n", o.AppVersion, v))
}
} else {
errMsg.WriteString("Missing \"version\" label\n")
}
if mv, ok := c.labels["minorversion"]; ok {
if _, err := strconv.Atoi(mv); err != nil {
errMsg.WriteString(fmt.Sprintln("label minorversion: not a numeric string:", mv))
}
} else {
errMsg.WriteString("Missing \"minorversion\" label\n")
}
if errMsg.Len() != 0 {
return errors.New(errMsg.String())
}
return nil
}
func TestIsAbortedError(t *testing.T) {
if !isAbortedError(abortedError) {
t.Errorf("isAborted(%+v): got false, want true", abortedError)
}
if isAbortedError(backendError) {
t.Errorf("isAborted(%+v): got true, want false", backendError)
}
}

View File

@@ -0,0 +1,460 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package valuecollector is used to collect the values of variables in a program.
package valuecollector
import (
"bytes"
"fmt"
"strconv"
"strings"
"golang.org/x/debug"
cd "google.golang.org/api/clouddebugger/v2"
)
const (
maxArrayLength = 50
maxMapLength = 20
)
// Collector is given references to variables from a program being debugged
// using AddVariable. Then when ReadValues is called, the Collector will fetch
// the values of those variables. Any variables referred to by those values
// will also be fetched; e.g. the targets of pointers, members of structs,
// elements of slices, etc. This continues iteratively, building a graph of
// values, until all the reachable values are fetched, or a size limit is
// reached.
//
// Variables are passed to the Collector as debug.Var, which is used by x/debug
// to represent references to variables. Values are returned as cd.Variable,
// which is used by the Debuglet Controller to represent the graph of values.
//
// For example, if the program has a struct variable:
//
// foo := SomeStruct{a:42, b:"xyz"}
//
// and we call AddVariable with a reference to foo, we will get back a result
// like:
//
// cd.Variable{Name:"foo", VarTableIndex:10}
//
// which denotes a variable named "foo" which will have its value stored in
// element 10 of the table that will later be returned by ReadValues. That
// element might be:
//
// out[10] = &cd.Variable{Members:{{Name:"a", VarTableIndex:11},{Name:"b", VarTableIndex:12}}}
//
// which denotes a struct with two members a and b, whose values are in elements
// 11 and 12 of the output table:
//
// out[11] = &cd.Variable{Value:"42"}
// out[12] = &cd.Variable{Value:"xyz"}
type Collector struct {
// prog is the program being debugged.
prog debug.Program
// limit is the maximum size of the output slice of values.
limit int
// index is a map from references (variables and map elements) to their
// locations in the table.
index map[reference]int
// table contains the references, including those given to the
// Collector directly and those the Collector itself found.
// If VarTableIndex is set to 0 in a cd.Variable, it is ignored, so the first entry
// of table can't be used. On initialization we put a dummy value there.
table []reference
}
// reference represents a value which is in the queue to be read by the
// collector. It is either a debug.Var, or a mapElement.
type reference interface{}
// mapElement represents an element of a map in the debugged program's memory.
type mapElement struct {
debug.Map
index uint64
}
// NewCollector returns a Collector for the given program and size limit.
// The limit is the maximum size of the slice of values returned by ReadValues.
func NewCollector(prog debug.Program, limit int) *Collector {
return &Collector{
prog: prog,
limit: limit,
index: make(map[reference]int),
table: []reference{debug.Var{}},
}
}
// AddVariable adds another variable to be collected.
// The Collector doesn't get the value immediately; it returns a cd.Variable
// that contains an index into the table which will later be returned by
// ReadValues.
func (c *Collector) AddVariable(lv debug.LocalVar) *cd.Variable {
ret := &cd.Variable{Name: lv.Name}
if index, ok := c.add(lv.Var); !ok {
// If the add call failed, it's because we reached the size limit.
// The Debuglet Controller's convention is to pass it a "Not Captured" error
// in this case.
ret.Status = statusMessage(messageNotCaptured, true, refersToVariableName)
} else {
ret.VarTableIndex = int64(index)
}
return ret
}
// add adds a reference to the set of values to be read from the
// program. It returns the index in the output table that will contain the
// corresponding value. It fails if the table has reached the size limit.
// It deduplicates references, so the index may be the same as one that was
// returned from an earlier add call.
func (c *Collector) add(r reference) (outputIndex int, ok bool) {
if i, ok := c.index[r]; ok {
return i, true
}
i := len(c.table)
if i >= c.limit {
return 0, false
}
c.index[r] = i
c.table = append(c.table, r)
return i, true
}
func addMember(v *cd.Variable, name string) *cd.Variable {
v2 := &cd.Variable{Name: name}
v.Members = append(v.Members, v2)
return v2
}
// ReadValues fetches values of the variables that were passed to the Collector
// with AddVariable. The values of any new variables found are also fetched,
// e.g. the targets of pointers or the members of structs, until we reach the
// size limit or we run out of values to fetch.
// The results are output as a []*cd.Variable, which is the type we need to send
// to the Debuglet Controller after we trigger a breakpoint.
func (c *Collector) ReadValues() (out []*cd.Variable) {
for i := 0; i < len(c.table); i++ {
// Create a new cd.Variable for this value, and append it to the output.
dcv := new(cd.Variable)
out = append(out, dcv)
if i == 0 {
// The first element is unused.
continue
}
switch x := c.table[i].(type) {
case mapElement:
key, value, err := c.prog.MapElement(x.Map, x.index)
if err != nil {
dcv.Status = statusMessage(err.Error(), true, refersToVariableValue)
continue
}
// Add a member for the key.
member := addMember(dcv, "key")
if index, ok := c.add(key); !ok {
// The table is full.
member.Status = statusMessage(messageNotCaptured, true, refersToVariableName)
continue
} else {
member.VarTableIndex = int64(index)
}
// Add a member for the value.
member = addMember(dcv, "value")
if index, ok := c.add(value); !ok {
// The table is full.
member.Status = statusMessage(messageNotCaptured, true, refersToVariableName)
} else {
member.VarTableIndex = int64(index)
}
case debug.Var:
if v, err := c.prog.Value(x); err != nil {
dcv.Status = statusMessage(err.Error(), true, refersToVariableValue)
} else {
c.FillValue(v, dcv)
}
}
}
return out
}
// indexable is an interface for arrays, slices and channels.
type indexable interface {
Len() uint64
Element(uint64) debug.Var
}
// channel implements indexable.
type channel struct {
debug.Channel
}
func (c channel) Len() uint64 {
return c.Length
}
var (
_ indexable = debug.Array{}
_ indexable = debug.Slice{}
_ indexable = channel{}
)
// FillValue copies a value into a cd.Variable. Any variables referred to by
// that value, e.g. struct members and pointer targets, are added to the
// collector's queue, to be fetched later by ReadValues.
func (c *Collector) FillValue(v debug.Value, dcv *cd.Variable) {
if c, ok := v.(debug.Channel); ok {
// Convert to channel, which implements indexable.
v = channel{c}
}
// Fill in dcv in a manner depending on the type of the value we got.
switch val := v.(type) {
case int8, int16, int32, int64, bool, uint8, uint16, uint32, uint64, float32, float64, complex64, complex128:
// For simple types, we just print the value to dcv.Value.
dcv.Value = fmt.Sprint(val)
case string:
// Put double quotes around strings.
dcv.Value = strconv.Quote(val)
case debug.String:
if uint64(len(val.String)) < val.Length {
// This string value was truncated.
dcv.Value = strconv.Quote(val.String + "...")
} else {
dcv.Value = strconv.Quote(val.String)
}
case debug.Struct:
// For structs, we add an entry to dcv.Members for each field in the
// struct.
// Each member will contain the name of the field, and the index in the
// output table which will contain the value of that field.
for _, f := range val.Fields {
member := addMember(dcv, f.Name)
if index, ok := c.add(f.Var); !ok {
// The table is full.
member.Status = statusMessage(messageNotCaptured, true, refersToVariableName)
} else {
member.VarTableIndex = int64(index)
}
}
case debug.Map:
dcv.Value = fmt.Sprintf("len = %d", val.Length)
for i := uint64(0); i < val.Length; i++ {
field := addMember(dcv, ``)
if i == maxMapLength {
field.Name = "..."
field.Status = statusMessage(messageTruncated, true, refersToVariableName)
break
}
if index, ok := c.add(mapElement{val, i}); !ok {
// The value table is full; add a member to contain the error message.
field.Name = "..."
field.Status = statusMessage(messageNotCaptured, true, refersToVariableName)
break
} else {
field.VarTableIndex = int64(index)
}
}
case debug.Pointer:
if val.Address == 0 {
dcv.Value = "<nil>"
} else if val.TypeID == 0 {
// We don't know the type of the pointer, so just output the address as
// the value.
dcv.Value = fmt.Sprintf("0x%X", val.Address)
dcv.Status = statusMessage(messageUnknownPointerType, false, refersToVariableName)
} else {
// Adds the pointed-to variable to the table, and links this value to
// that table entry through VarTableIndex.
dcv.Value = fmt.Sprintf("0x%X", val.Address)
target := addMember(dcv, "")
if index, ok := c.add(debug.Var(val)); !ok {
target.Status = statusMessage(messageNotCaptured, true, refersToVariableName)
} else {
target.VarTableIndex = int64(index)
}
}
case indexable:
// Arrays, slices and channels.
dcv.Value = "len = " + fmt.Sprint(val.Len())
for j := uint64(0); j < val.Len(); j++ {
field := addMember(dcv, fmt.Sprint(`[`, j, `]`))
if j == maxArrayLength {
field.Name = "..."
field.Status = statusMessage(messageTruncated, true, refersToVariableName)
break
}
vr := val.Element(j)
if index, ok := c.add(vr); !ok {
// The value table is full; add a member to contain the error message.
field.Name = "..."
field.Status = statusMessage(messageNotCaptured, true, refersToVariableName)
break
} else {
// Add a member with the index as the name.
field.VarTableIndex = int64(index)
}
}
default:
dcv.Status = statusMessage(messageUnknownType, false, refersToVariableName)
}
}
// statusMessage returns a *cd.StatusMessage with the given message, IsError
// field and refersTo field.
func statusMessage(msg string, isError bool, refersTo int) *cd.StatusMessage {
return &cd.StatusMessage{
Description: &cd.FormatMessage{Format: "$0", Parameters: []string{msg}},
IsError: isError,
RefersTo: refersToString[refersTo],
}
}
// LogString produces a string for a logpoint, substituting in variable values
// using evaluatedExpressions and varTable.
func LogString(s string, evaluatedExpressions []*cd.Variable, varTable []*cd.Variable) string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "LOGPOINT: ")
seen := make(map[*cd.Variable]bool)
for i := 0; i < len(s); {
if s[i] == '$' {
i++
if num, n, ok := parseToken(s[i:], len(evaluatedExpressions)-1); ok {
// This token is one of $0, $1, etc. Write the corresponding expression.
writeExpression(&buf, evaluatedExpressions[num], false, varTable, seen)
i += n
} else {
// Something else, like $$.
buf.WriteByte(s[i])
i++
}
} else {
buf.WriteByte(s[i])
i++
}
}
return buf.String()
}
func parseToken(s string, max int) (num int, bytesRead int, ok bool) {
var i int
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
i++
}
num, err := strconv.Atoi(s[:i])
return num, i, err == nil && num <= max
}
// writeExpression recursively writes variables to buf, in a format suitable
// for logging. If printName is true, writes the name of the variable.
func writeExpression(buf *bytes.Buffer, v *cd.Variable, printName bool, varTable []*cd.Variable, seen map[*cd.Variable]bool) {
if v == nil {
// Shouldn't happen.
return
}
name, value, status, members := v.Name, v.Value, v.Status, v.Members
// If v.VarTableIndex is not zero, it refers to an element of varTable.
// We merge its fields with the fields we got from v.
var other *cd.Variable
if idx := int(v.VarTableIndex); idx > 0 && idx < len(varTable) {
other = varTable[idx]
}
if other != nil {
if name == "" {
name = other.Name
}
if value == "" {
value = other.Value
}
if status == nil {
status = other.Status
}
if len(members) == 0 {
members = other.Members
}
}
if printName && name != "" {
buf.WriteString(name)
buf.WriteByte(':')
}
// If we have seen this value before, write "..." rather than repeating it.
if seen[v] {
buf.WriteString("...")
return
}
seen[v] = true
if other != nil {
if seen[other] {
buf.WriteString("...")
return
}
seen[other] = true
}
if value != "" && !strings.HasPrefix(value, "len = ") {
// A plain value.
buf.WriteString(value)
} else if status != nil && status.Description != nil {
// An error.
for _, p := range status.Description.Parameters {
buf.WriteByte('(')
buf.WriteString(p)
buf.WriteByte(')')
}
} else if name == `` {
// A map element.
first := true
for _, member := range members {
if first {
first = false
} else {
buf.WriteByte(':')
}
writeExpression(buf, member, false, varTable, seen)
}
} else {
// A map, array, slice, channel, or struct.
isStruct := value == ""
first := true
buf.WriteByte('{')
for _, member := range members {
if first {
first = false
} else {
buf.WriteString(", ")
}
writeExpression(buf, member, isStruct, varTable, seen)
}
buf.WriteByte('}')
}
}
const (
// Error messages for cd.StatusMessage
messageNotCaptured = "Not captured"
messageTruncated = "Truncated"
messageUnknownPointerType = "Unknown pointer type"
messageUnknownType = "Unknown type"
// RefersTo values for cd.StatusMessage.
refersToVariableName = iota
refersToVariableValue
)
// refersToString contains the strings for each refersTo value.
// See the definition of StatusMessage in the v2/clouddebugger package.
var refersToString = map[int]string{
refersToVariableName: "VARIABLE_NAME",
refersToVariableValue: "VARIABLE_VALUE",
}

View File

@@ -0,0 +1,418 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package valuecollector
import (
"fmt"
"reflect"
"testing"
"golang.org/x/debug"
cd "google.golang.org/api/clouddebugger/v2"
)
const (
// Some arbitrary type IDs for the test, for use in debug.Var's TypeID field.
// A TypeID of 0 means the type is unknown, so we start at 1.
int16Type = iota + 1
stringType
structType
pointerType
arrayType
int32Type
debugStringType
mapType
channelType
sliceType
)
func TestValueCollector(t *testing.T) {
// Construct the collector.
c := NewCollector(&Program{}, 26)
// Add some variables of various types, whose values we want the collector to read.
variablesToAdd := []debug.LocalVar{
{Name: "a", Var: debug.Var{int16Type, 0x1}},
{Name: "b", Var: debug.Var{stringType, 0x2}},
{Name: "c", Var: debug.Var{structType, 0x3}},
{Name: "d", Var: debug.Var{pointerType, 0x4}},
{Name: "e", Var: debug.Var{arrayType, 0x5}},
{Name: "f", Var: debug.Var{debugStringType, 0x6}},
{Name: "g", Var: debug.Var{mapType, 0x7}},
{Name: "h", Var: debug.Var{channelType, 0x8}},
{Name: "i", Var: debug.Var{sliceType, 0x9}},
}
expectedResults := []*cd.Variable{
&cd.Variable{Name: "a", VarTableIndex: 1},
&cd.Variable{Name: "b", VarTableIndex: 2},
&cd.Variable{Name: "c", VarTableIndex: 3},
&cd.Variable{Name: "d", VarTableIndex: 4},
&cd.Variable{Name: "e", VarTableIndex: 5},
&cd.Variable{Name: "f", VarTableIndex: 6},
&cd.Variable{Name: "g", VarTableIndex: 7},
&cd.Variable{Name: "h", VarTableIndex: 8},
&cd.Variable{Name: "i", VarTableIndex: 9},
}
for i, v := range variablesToAdd {
added := c.AddVariable(v)
if !reflect.DeepEqual(added, expectedResults[i]) {
t.Errorf("AddVariable: got %+v want %+v", *added, *expectedResults[i])
}
}
// Read the values, compare the output to what we expect.
v := c.ReadValues()
expectedValues := []*cd.Variable{
&cd.Variable{},
&cd.Variable{Value: "1"},
&cd.Variable{Value: `"hello"`},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "x", VarTableIndex: 1},
&cd.Variable{Name: "y", VarTableIndex: 2},
},
},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{VarTableIndex: 1},
},
Value: "0x1",
},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "[0]", VarTableIndex: 10},
&cd.Variable{Name: "[1]", VarTableIndex: 11},
&cd.Variable{Name: "[2]", VarTableIndex: 12},
&cd.Variable{Name: "[3]", VarTableIndex: 13},
},
Value: "len = 4",
},
&cd.Variable{Value: `"world"`},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "⚫", VarTableIndex: 14},
&cd.Variable{Name: "⚫", VarTableIndex: 15},
&cd.Variable{Name: "⚫", VarTableIndex: 16},
},
Value: "len = 3",
},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "[0]", VarTableIndex: 17},
&cd.Variable{Name: "[1]", VarTableIndex: 18},
},
Value: "len = 2",
},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "[0]", VarTableIndex: 19},
&cd.Variable{Name: "[1]", VarTableIndex: 20},
},
Value: "len = 2",
},
&cd.Variable{Value: "100"},
&cd.Variable{Value: "104"},
&cd.Variable{Value: "108"},
&cd.Variable{Value: "112"},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "key", VarTableIndex: 21},
&cd.Variable{Name: "value", VarTableIndex: 22},
},
},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "key", VarTableIndex: 23},
&cd.Variable{Name: "value", VarTableIndex: 24},
},
},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "key", VarTableIndex: 25},
&cd.Variable{
Name: "value",
Status: &cd.StatusMessage{
Description: &cd.FormatMessage{
Format: "$0",
Parameters: []string{"Not captured"},
},
IsError: true,
RefersTo: "VARIABLE_NAME",
},
},
},
},
&cd.Variable{Value: "246"},
&cd.Variable{Value: "210"},
&cd.Variable{Value: "300"},
&cd.Variable{Value: "304"},
&cd.Variable{Value: "400"},
&cd.Variable{Value: "404"},
&cd.Variable{Value: "1400"},
&cd.Variable{Value: "1404"},
&cd.Variable{Value: "2400"},
}
if !reflect.DeepEqual(v, expectedValues) {
t.Errorf("ReadValues: got %v want %v", v, expectedValues)
// Do element-by-element comparisons, for more useful error messages.
for i := range v {
if i < len(expectedValues) && !reflect.DeepEqual(v[i], expectedValues[i]) {
t.Errorf("element %d: got %+v want %+v", i, *v[i], *expectedValues[i])
}
}
}
}
// Program implements the similarly-named interface in x/debug.
// ValueCollector should only call its Value and MapElement methods.
type Program struct {
debug.Program
}
func (p *Program) Value(v debug.Var) (debug.Value, error) {
// We determine what to return using v.TypeID.
switch v.TypeID {
case int16Type:
// We use the address as the value, so that we're testing whether the right
// address was calculated.
return int16(v.Address), nil
case stringType:
// A string.
return "hello", nil
case structType:
// A struct with two elements.
return debug.Struct{
Fields: []debug.StructField{
{
Name: "x",
Var: debug.Var{int16Type, 0x1},
},
{
Name: "y",
Var: debug.Var{stringType, 0x2},
},
},
}, nil
case pointerType:
// A pointer to the first variable above.
return debug.Pointer{int16Type, 0x1}, nil
case arrayType:
// An array of 4 32-bit-wide elements.
return debug.Array{
ElementTypeID: int32Type,
Address: 0x64,
Length: 4,
StrideBits: 32,
}, nil
case debugStringType:
return debug.String{
Length: 5,
String: "world",
}, nil
case mapType:
return debug.Map{
TypeID: 99,
Address: 0x100,
Length: 3,
}, nil
case channelType:
return debug.Channel{
ElementTypeID: int32Type,
Address: 200,
Buffer: 210,
Length: 2,
Capacity: 10,
Stride: 4,
BufferStart: 9,
}, nil
case sliceType:
// A slice of 2 32-bit-wide elements.
return debug.Slice{
Array: debug.Array{
ElementTypeID: int32Type,
Address: 300,
Length: 2,
StrideBits: 32,
},
Capacity: 50,
}, nil
case int32Type:
// We use the address as the value, so that we're testing whether the right
// address was calculated.
return int32(v.Address), nil
}
return nil, fmt.Errorf("unexpected Value request")
}
func (p *Program) MapElement(m debug.Map, index uint64) (debug.Var, debug.Var, error) {
return debug.Var{TypeID: int16Type, Address: 1000*index + 400},
debug.Var{TypeID: int32Type, Address: 1000*index + 404},
nil
}
func TestLogString(t *testing.T) {
bp := cd.Breakpoint{
Action: "LOG",
LogMessageFormat: "$0 hello, $$7world! $1 $2 $3 $4 $5$6 $7 $8",
EvaluatedExpressions: []*cd.Variable{
&cd.Variable{Name: "a", VarTableIndex: 1},
&cd.Variable{Name: "b", VarTableIndex: 2},
&cd.Variable{Name: "c", VarTableIndex: 3},
&cd.Variable{Name: "d", VarTableIndex: 4},
&cd.Variable{Name: "e", VarTableIndex: 5},
&cd.Variable{Name: "f", VarTableIndex: 6},
&cd.Variable{Name: "g", VarTableIndex: 7},
&cd.Variable{Name: "h", VarTableIndex: 8},
&cd.Variable{Name: "i", VarTableIndex: 9},
},
}
varTable := []*cd.Variable{
&cd.Variable{},
&cd.Variable{Value: "1"},
&cd.Variable{Value: `"hello"`},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "x", Value: "1"},
&cd.Variable{Name: "y", Value: `"hello"`},
&cd.Variable{Name: "z", VarTableIndex: 3},
},
},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{VarTableIndex: 1},
},
Value: "0x1",
},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "[0]", VarTableIndex: 10},
&cd.Variable{Name: "[1]", VarTableIndex: 11},
&cd.Variable{Name: "[2]", VarTableIndex: 12},
&cd.Variable{Name: "[3]", VarTableIndex: 13},
},
Value: "len = 4",
},
&cd.Variable{Value: `"world"`},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "⚫", VarTableIndex: 14},
&cd.Variable{Name: "⚫", VarTableIndex: 15},
&cd.Variable{Name: "⚫", VarTableIndex: 16},
},
Value: "len = 3",
},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "[0]", VarTableIndex: 17},
&cd.Variable{Name: "[1]", VarTableIndex: 18},
},
Value: "len = 2",
},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "[0]", VarTableIndex: 19},
&cd.Variable{Name: "[1]", VarTableIndex: 20},
},
Value: "len = 2",
},
&cd.Variable{Value: "100"},
&cd.Variable{Value: "104"},
&cd.Variable{Value: "108"},
&cd.Variable{Value: "112"},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "key", VarTableIndex: 21},
&cd.Variable{Name: "value", VarTableIndex: 22},
},
},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "key", VarTableIndex: 23},
&cd.Variable{Name: "value", VarTableIndex: 24},
},
},
&cd.Variable{
Members: []*cd.Variable{
&cd.Variable{Name: "key", VarTableIndex: 25},
&cd.Variable{
Name: "value",
Status: &cd.StatusMessage{
Description: &cd.FormatMessage{
Format: "$0",
Parameters: []string{"Not captured"},
},
IsError: true,
RefersTo: "VARIABLE_NAME",
},
},
},
},
&cd.Variable{Value: "246"},
&cd.Variable{Value: "210"},
&cd.Variable{Value: "300"},
&cd.Variable{Value: "304"},
&cd.Variable{Value: "400"},
&cd.Variable{Value: "404"},
&cd.Variable{Value: "1400"},
&cd.Variable{Value: "1404"},
&cd.Variable{Value: "2400"},
}
s := LogString(bp.LogMessageFormat, bp.EvaluatedExpressions, varTable)
expected := `LOGPOINT: 1 hello, $7world! "hello" {x:1, y:"hello", z:...} ` +
`0x1 {100, 104, 108, 112} "world"{400:404, 1400:1404, 2400:(Not captured)} ` +
`{246, 210} {300, 304}`
if s != expected {
t.Errorf("LogString: got %q want %q", s, expected)
}
}
func TestParseToken(t *testing.T) {
for _, c := range []struct {
s string
max int
num int
n int
ok bool
}{
{"", 0, 0, 0, false},
{".", 0, 0, 0, false},
{"0", 0, 0, 1, true},
{"0", 1, 0, 1, true},
{"00", 0, 0, 2, true},
{"1.", 1, 1, 1, true},
{"1.", 0, 0, 0, false},
{"10", 10, 10, 2, true},
{"10..", 10, 10, 2, true},
{"10", 11, 10, 2, true},
{"10..", 11, 10, 2, true},
{"10", 9, 0, 0, false},
{"10..", 9, 0, 0, false},
{" 10", 10, 0, 0, false},
{"010", 10, 10, 3, true},
{"123456789", 123456789, 123456789, 9, true},
{"123456789", 123456788, 0, 0, false},
{"123456789123456789123456789", 999999999, 0, 0, false},
} {
num, n, ok := parseToken(c.s, c.max)
if ok != c.ok {
t.Errorf("parseToken(%q, %d): got ok=%t want ok=%t", c.s, c.max, ok, c.ok)
continue
}
if !ok {
continue
}
if num != c.num || n != c.n {
t.Errorf("parseToken(%q, %d): got %d,%d,%t want %d,%d,%t", c.s, c.max, num, n, ok, c.num, c.n, c.ok)
}
}
}