hdx/vendor/cloud.google.com/go/cmd/go-cloud-debug-agent/debuglet.go
Derek McQuay 59af8fc84f
initial commit
Signed-off-by: Derek McQuay <derekmcquay@gmail.com>
2018-04-10 19:17:26 -07:00

451 lines
15 KiB
Go

// 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,go1.7
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
}