// Copyright 2016 Google LLC // // 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 ( "context" "crypto/sha256" "encoding/json" "errors" "fmt" "log" "sync" "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 }