| // 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 | |
| } |