1// Copyright 2016 Google LLC 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// Package controller is a library for interacting with the Google Cloud Debugger's Debuglet Controller service. 16package controller 17 18import ( 19 "context" 20 "crypto/sha256" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "log" 25 "sync" 26 27 "golang.org/x/oauth2" 28 cd "google.golang.org/api/clouddebugger/v2" 29 "google.golang.org/api/googleapi" 30 "google.golang.org/api/option" 31 htransport "google.golang.org/api/transport/http" 32) 33 34const ( 35 // agentVersionString identifies the agent to the service. 36 agentVersionString = "google.com/go-gcp/v0.2" 37 // initWaitToken is the wait token sent in the first Update request to a server. 38 initWaitToken = "init" 39) 40 41var ( 42 // ErrListUnchanged is returned by List if the server time limit is reached 43 // before the list of breakpoints changes. 44 ErrListUnchanged = errors.New("breakpoint list unchanged") 45 // ErrDebuggeeDisabled is returned by List or Update if the server has disabled 46 // this Debuggee. The caller can retry later. 47 ErrDebuggeeDisabled = errors.New("debuglet disabled by server") 48) 49 50// Controller manages a connection to the Debuglet Controller service. 51type Controller struct { 52 s serviceInterface 53 // waitToken is sent with List requests so the server knows which set of 54 // breakpoints this client has already seen. Each successful List request 55 // returns a new waitToken to send in the next request. 56 waitToken string 57 // verbose determines whether to do some logging 58 verbose bool 59 // options, uniquifier and description are used in register. 60 options Options 61 uniquifier string 62 description string 63 // labels are included when registering the debuggee. They should contain 64 // the module name, version and minorversion, and are used by the debug UI 65 // to label the correct version active for debugging. 66 labels map[string]string 67 // mu protects debuggeeID 68 mu sync.Mutex 69 // debuggeeID is returned from the server on registration, and is passed back 70 // to the server in List and Update requests. 71 debuggeeID string 72} 73 74// Options controls how the Debuglet Controller client identifies itself to the server. 75// See https://cloud.google.com/storage/docs/projects and 76// https://cloud.google.com/tools/cloud-debugger/setting-up-on-compute-engine 77// for further documentation of these parameters. 78type Options struct { 79 ProjectNumber string // GCP Project Number. 80 ProjectID string // GCP Project ID. 81 AppModule string // Module name for the debugged program. 82 AppVersion string // Version number for this module. 83 SourceContexts []*cd.SourceContext // Description of source. 84 Verbose bool 85 TokenSource oauth2.TokenSource // Source of Credentials used for Stackdriver Debugger. 86} 87 88type serviceInterface interface { 89 Register(ctx context.Context, req *cd.RegisterDebuggeeRequest) (*cd.RegisterDebuggeeResponse, error) 90 Update(ctx context.Context, debuggeeID, breakpointID string, req *cd.UpdateActiveBreakpointRequest) (*cd.UpdateActiveBreakpointResponse, error) 91 List(ctx context.Context, debuggeeID, waitToken string) (*cd.ListActiveBreakpointsResponse, error) 92} 93 94var newService = func(ctx context.Context, tokenSource oauth2.TokenSource) (serviceInterface, error) { 95 httpClient, endpoint, err := htransport.NewClient(ctx, option.WithTokenSource(tokenSource)) 96 if err != nil { 97 return nil, err 98 } 99 s, err := cd.New(httpClient) 100 if err != nil { 101 return nil, err 102 } 103 if endpoint != "" { 104 s.BasePath = endpoint 105 } 106 return &service{s: s}, nil 107} 108 109type service struct { 110 s *cd.Service 111} 112 113func (s service) Register(ctx context.Context, req *cd.RegisterDebuggeeRequest) (*cd.RegisterDebuggeeResponse, error) { 114 call := cd.NewControllerDebuggeesService(s.s).Register(req) 115 return call.Context(ctx).Do() 116} 117 118func (s service) Update(ctx context.Context, debuggeeID, breakpointID string, req *cd.UpdateActiveBreakpointRequest) (*cd.UpdateActiveBreakpointResponse, error) { 119 call := cd.NewControllerDebuggeesBreakpointsService(s.s).Update(debuggeeID, breakpointID, req) 120 return call.Context(ctx).Do() 121} 122 123func (s service) List(ctx context.Context, debuggeeID, waitToken string) (*cd.ListActiveBreakpointsResponse, error) { 124 call := cd.NewControllerDebuggeesBreakpointsService(s.s).List(debuggeeID) 125 call.WaitToken(waitToken) 126 return call.Context(ctx).Do() 127} 128 129// NewController connects to the Debuglet Controller server using the given options, 130// and returns a Controller for that connection. 131// Google Application Default Credentials are used to connect to the Debuglet Controller; 132// see https://developers.google.com/identity/protocols/application-default-credentials 133func NewController(ctx context.Context, o Options) (*Controller, error) { 134 // We build a JSON encoding of o.SourceContexts so we can hash it. 135 scJSON, err := json.Marshal(o.SourceContexts) 136 if err != nil { 137 scJSON = nil 138 o.SourceContexts = nil 139 } 140 const minorversion = "107157" // any arbitrary numeric string 141 142 // Compute a uniquifier string by hashing the project number, app module name, 143 // app module version, debuglet version, and source context. 144 // The choice of hash function is arbitrary. 145 h := sha256.Sum256([]byte(fmt.Sprintf("%d %s %d %s %d %s %d %s %d %s %d %s", 146 len(o.ProjectNumber), o.ProjectNumber, 147 len(o.AppModule), o.AppModule, 148 len(o.AppVersion), o.AppVersion, 149 len(agentVersionString), agentVersionString, 150 len(scJSON), scJSON, 151 len(minorversion), minorversion))) 152 uniquifier := fmt.Sprintf("%X", h[0:16]) // 32 hex characters 153 154 description := o.ProjectID 155 if o.AppModule != "" { 156 description += "-" + o.AppModule 157 } 158 if o.AppVersion != "" { 159 description += "-" + o.AppVersion 160 } 161 162 s, err := newService(ctx, o.TokenSource) 163 if err != nil { 164 return nil, err 165 } 166 167 // Construct client. 168 c := &Controller{ 169 s: s, 170 waitToken: initWaitToken, 171 verbose: o.Verbose, 172 options: o, 173 uniquifier: uniquifier, 174 description: description, 175 labels: map[string]string{ 176 "module": o.AppModule, 177 "version": o.AppVersion, 178 "minorversion": minorversion, 179 }, 180 } 181 182 return c, nil 183} 184 185func (c *Controller) getDebuggeeID(ctx context.Context) (string, error) { 186 c.mu.Lock() 187 defer c.mu.Unlock() 188 if c.debuggeeID != "" { 189 return c.debuggeeID, nil 190 } 191 // The debuglet hasn't been registered yet, or it is disabled and we should try registering again. 192 if err := c.register(ctx); err != nil { 193 return "", err 194 } 195 return c.debuggeeID, nil 196} 197 198// List retrieves the current list of breakpoints from the server. 199// If the set of breakpoints on the server is the same as the one returned in 200// the previous call to List, the server can delay responding until it changes, 201// and return an error instead if no change occurs before a time limit the 202// server sets. List can't be called concurrently with itself. 203func (c *Controller) List(ctx context.Context) (*cd.ListActiveBreakpointsResponse, error) { 204 id, err := c.getDebuggeeID(ctx) 205 if err != nil { 206 return nil, err 207 } 208 resp, err := c.s.List(ctx, id, c.waitToken) 209 if err != nil { 210 if isAbortedError(err) { 211 return nil, ErrListUnchanged 212 } 213 // For other errors, the protocol requires that we attempt to re-register. 214 c.mu.Lock() 215 defer c.mu.Unlock() 216 if regError := c.register(ctx); regError != nil { 217 return nil, regError 218 } 219 return nil, err 220 } 221 if resp == nil { 222 return nil, errors.New("no response") 223 } 224 if c.verbose { 225 log.Printf("List response: %v", resp) 226 } 227 c.waitToken = resp.NextWaitToken 228 return resp, nil 229} 230 231// isAbortedError tests if err is a *googleapi.Error, that it contains one error 232// in Errors, and that that error's Reason is "aborted". 233func isAbortedError(err error) bool { 234 e, _ := err.(*googleapi.Error) 235 if e == nil { 236 return false 237 } 238 if len(e.Errors) != 1 { 239 return false 240 } 241 return e.Errors[0].Reason == "aborted" 242} 243 244// Update reports information to the server about a breakpoint that was hit. 245// Update can be called concurrently with List and Update. 246func (c *Controller) Update(ctx context.Context, breakpointID string, bp *cd.Breakpoint) error { 247 req := &cd.UpdateActiveBreakpointRequest{Breakpoint: bp} 248 if c.verbose { 249 log.Printf("sending update for %s: %v", breakpointID, req) 250 } 251 id, err := c.getDebuggeeID(ctx) 252 if err != nil { 253 return err 254 } 255 _, err = c.s.Update(ctx, id, breakpointID, req) 256 return err 257} 258 259// register calls the Debuglet Controller Register method, and sets c.debuggeeID. 260// c.mu should be locked while calling this function. List and Update can't 261// make progress until it returns. 262func (c *Controller) register(ctx context.Context) error { 263 req := cd.RegisterDebuggeeRequest{ 264 Debuggee: &cd.Debuggee{ 265 AgentVersion: agentVersionString, 266 Description: c.description, 267 Project: c.options.ProjectNumber, 268 SourceContexts: c.options.SourceContexts, 269 Uniquifier: c.uniquifier, 270 Labels: c.labels, 271 }, 272 } 273 resp, err := c.s.Register(ctx, &req) 274 if err != nil { 275 return err 276 } 277 if resp == nil { 278 return errors.New("register: no response") 279 } 280 if resp.Debuggee.IsDisabled { 281 // Setting c.debuggeeID to empty makes sure future List and Update calls 282 // will call register first. 283 c.debuggeeID = "" 284 } else { 285 c.debuggeeID = resp.Debuggee.Id 286 } 287 if c.debuggeeID == "" { 288 return ErrDebuggeeDisabled 289 } 290 return nil 291} 292