1# -*- coding: utf-8 -*- # 2# Copyright 2016 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Debug apis layer.""" 17 18from __future__ import absolute_import 19from __future__ import division 20from __future__ import unicode_literals 21 22import re 23import threading 24 25from apitools.base.py import exceptions as apitools_exceptions 26 27from googlecloudsdk.api_lib.debug import errors 28from googlecloudsdk.api_lib.util import apis 29from googlecloudsdk.core import config 30from googlecloudsdk.core import log 31from googlecloudsdk.core import resources 32from googlecloudsdk.core.util import retry 33 34import six 35from six.moves import urllib 36 37# Names for default module and version. In App Engine, the default module and 38# version don't report explicit names to the debugger, so use these strings 39# instead when displaying the target name. Note that this code assumes there 40# will not be a non-default version or module explicitly named 'default', since 41# that would result in a naming conflict between the actual default and the 42# one named 'default'. 43DEFAULT_MODULE = 'default' 44DEFAULT_VERSION = 'default' 45 46 47def SplitLogExpressions(format_string): 48 """Extracts {expression} substrings into a separate array. 49 50 Each substring of the form {expression} will be extracted into an array, and 51 each {expression} substring will be replaced with $N, where N is the index 52 of the extraced expression in the array. Any '$' sequence outside an 53 expression will be escaped with '$$'. 54 55 For example, given the input: 56 'a={a}, b={b}' 57 The return value would be: 58 ('a=$0, b=$1', ['a', 'b']) 59 60 Args: 61 format_string: The string to process. 62 Returns: 63 string, [string] - The new format string and the array of expressions. 64 Raises: 65 InvalidLogFormatException: if the string has unbalanced braces. 66 """ 67 expressions = [] 68 log_format = '' 69 current_expression = '' 70 brace_count = 0 71 need_separator = False 72 for c in format_string: 73 if need_separator and c.isdigit(): 74 log_format += ' ' 75 need_separator = False 76 if c == '{': 77 if brace_count: 78 # Nested braces 79 current_expression += c 80 else: 81 # New expression 82 current_expression = '' 83 brace_count += 1 84 elif not brace_count: 85 if c == '}': 86 # Unbalanced left brace. 87 raise errors.InvalidLogFormatException( 88 'There are too many "}" characters in the log format string') 89 elif c == '$': 90 # Escape '$' 91 log_format += '$$' 92 else: 93 # Not in or starting an expression. 94 log_format += c 95 else: 96 # Currently reading an expression. 97 if c != '}': 98 current_expression += c 99 continue 100 brace_count -= 1 101 if brace_count == 0: 102 # Finish processing the expression 103 if current_expression in expressions: 104 i = expressions.index(current_expression) 105 else: 106 i = len(expressions) 107 expressions.append(current_expression) 108 log_format += '${0}'.format(i) 109 # If the next character is a digit, we need an extra space to prevent 110 # the agent from combining the positional argument with the subsequent 111 # digits. 112 need_separator = True 113 else: 114 # Closing a nested brace 115 current_expression += c 116 117 if brace_count: 118 # Unbalanced left brace. 119 raise errors.InvalidLogFormatException( 120 'There are too many "{" characters in the log format string') 121 return log_format, expressions 122 123 124def MergeLogExpressions(log_format, expressions): 125 """Replaces each $N substring with the corresponding {expression}. 126 127 This function is intended for reconstructing an input expression string that 128 has been split using SplitLogExpressions. It is not intended for substituting 129 the expression results at log time. 130 131 Args: 132 log_format: A string containing 0 or more $N substrings, where N is any 133 valid index into the expressions array. Each such substring will be 134 replaced by '{expression}', where "expression" is expressions[N]. 135 expressions: The expressions to substitute into the format string. 136 Returns: 137 The combined string. 138 """ 139 def GetExpression(m): 140 try: 141 return '{{{0}}}'.format(expressions[int(m.group(0)[1:])]) 142 except IndexError: 143 return m.group(0) 144 145 parts = log_format.split('$$') 146 return '$'.join(re.sub(r'\$\d+', GetExpression, part) for part in parts) 147 148 149def DebugViewUrl(breakpoint): 150 """Returns a URL to view a breakpoint in the browser. 151 152 Given a breakpoint, this transform will return a URL which will open the 153 snapshot's location in a debug view pointing at the snapshot. 154 155 Args: 156 breakpoint: A breakpoint object with added information on project and 157 debug target. 158 Returns: 159 The URL for the breakpoint. 160 """ 161 debug_view_url = 'https://console.cloud.google.com/debug/fromgcloud?' 162 data = [ 163 ('project', breakpoint.project), 164 ('dbgee', breakpoint.target_id), 165 ('bp', breakpoint.id) 166 ] 167 return debug_view_url + urllib.parse.urlencode(data) 168 169 170def LogQueryV2String(breakpoint, separator=' '): 171 """Returns an advanced log query string for use with gcloud logging read. 172 173 Args: 174 breakpoint: A breakpoint object with added information on project, service, 175 and debug target. 176 separator: A string to append between conditions 177 Returns: 178 A log query suitable for use with gcloud logging read. 179 Raises: 180 InvalidLogFormatException if the breakpoint has an invalid log expression. 181 """ 182 query = ( 183 'resource.type=gae_app{sep}' 184 'logName:request_log{sep}' 185 'resource.labels.module_id="{service}"{sep}' 186 'resource.labels.version_id="{version}"{sep}' 187 'severity={logLevel}').format( 188 service=breakpoint.service, version=breakpoint.version, 189 logLevel=breakpoint.logLevel or 'INFO', sep=separator) 190 if breakpoint.logMessageFormat: 191 # Search for all of the non-expression components of the message. 192 # The re.sub converts the format to a series of quoted strings. 193 query += '{sep}"{text}"'.format( 194 text=re.sub(r'\$([0-9]+)', r'" "', 195 SplitLogExpressions(breakpoint.logMessageFormat)[0]), 196 sep=separator) 197 return query 198 199 200def LogViewUrl(breakpoint): 201 """Returns a URL to view the output for a logpoint. 202 203 Given a breakpoint in an appengine service, this transform will return a URL 204 which will open the log viewer to the request log for the service. 205 206 Args: 207 breakpoint: A breakpoint object with added information on project, service, 208 debug target, and logQuery. 209 Returns: 210 The URL for the appropriate logs. 211 """ 212 debug_view_url = 'https://console.cloud.google.com/logs?' 213 data = [ 214 ('project', breakpoint.project), 215 ('advancedFilter', LogQueryV2String(breakpoint, separator='\n') + '\n') 216 ] 217 return debug_view_url + urllib.parse.urlencode(data) 218 219 220class DebugObject(object): 221 """Base class for debug api wrappers.""" 222 223 # Lock for remote calls in routines which might be multithreaded. Client 224 # connections are not thread-safe. Currently, only WaitForBreakpoint can 225 # be called from multiple threads. 226 _client_lock = threading.Lock() 227 228 # Breakpoint type name constants 229 SNAPSHOT_TYPE = 'SNAPSHOT' 230 LOGPOINT_TYPE = 'LOGPOINT' 231 232 def BreakpointAction(self, type_name): 233 if type_name == self.SNAPSHOT_TYPE: 234 return self._debug_messages.Breakpoint.ActionValueValuesEnum.CAPTURE 235 if type_name == self.LOGPOINT_TYPE: 236 return self._debug_messages.Breakpoint.ActionValueValuesEnum.LOG 237 raise errors.InvalidBreakpointTypeError(type_name) 238 239 CLIENT_VERSION = 'google.com/gcloud/{0}'.format(config.CLOUD_SDK_VERSION) 240 241 def __init__(self, debug_client=None, debug_messages=None, 242 resource_client=None, resource_messages=None): 243 """Sets up class with instantiated api client.""" 244 self._debug_client = ( 245 debug_client or apis.GetClientInstance('clouddebugger', 'v2')) 246 self._debug_messages = ( 247 debug_messages or apis.GetMessagesModule('clouddebugger', 'v2')) 248 self._resource_client = ( 249 resource_client or 250 apis.GetClientInstance('cloudresourcemanager', 'v1beta1')) 251 self._resource_messages = ( 252 resource_messages or 253 apis.GetMessagesModule('cloudresourcemanager', 'v1beta1')) 254 self._resource_parser = resources.REGISTRY.Clone() 255 self._resource_parser.RegisterApiByName('clouddebugger', 'v2') 256 257 258class Debugger(DebugObject): 259 """Abstracts Cloud Debugger service for a project.""" 260 261 def __init__(self, project, debug_client=None, debug_messages=None, 262 resource_client=None, resource_messages=None): 263 super(Debugger, self).__init__( 264 debug_client=debug_client, debug_messages=debug_messages, 265 resource_client=resource_client, resource_messages=resource_messages) 266 self._project = project 267 268 def ListDebuggees(self, include_inactive=False, include_stale=False): 269 """Lists all debug targets registered with the debug service. 270 271 Args: 272 include_inactive: If true, also include debuggees that are not currently 273 running. 274 include_stale: If false, filter out any debuggees that refer to 275 stale minor versions. A debugge represents a stale minor version if it 276 meets the following criteria: 277 1. It has a minorversion label. 278 2. All other debuggees with the same name (i.e., all debuggees with 279 the same module and version, in the case of app engine) have a 280 minorversion label. 281 3. The minorversion value for the debuggee is less than the 282 minorversion value for at least one other debuggee with the same 283 name. 284 Returns: 285 [Debuggee] A list of debuggees. 286 """ 287 request = self._debug_messages.ClouddebuggerDebuggerDebuggeesListRequest( 288 project=self._project, includeInactive=include_inactive, 289 clientVersion=self.CLIENT_VERSION) 290 try: 291 response = self._debug_client.debugger_debuggees.List(request) 292 except apitools_exceptions.HttpError as error: 293 raise errors.UnknownHttpError(error) 294 295 result = [Debuggee(debuggee) for debuggee in response.debuggees] 296 297 if not include_stale: 298 return _FilterStaleMinorVersions(result) 299 300 return result 301 302 def DefaultDebuggee(self): 303 """Find the default debuggee. 304 305 Returns: 306 The default debug target, which is either the only target available 307 or the latest minor version of the application, if all targets have the 308 same module and version. 309 Raises: 310 errors.NoDebuggeeError if no debuggee was found. 311 errors.MultipleDebuggeesError if there is not a unique default. 312 """ 313 debuggees = self.ListDebuggees() 314 if len(debuggees) == 1: 315 # Just one possible target 316 return debuggees[0] 317 318 if not debuggees: 319 raise errors.NoDebuggeeError() 320 321 # More than one module or version. Can't determine the default target. 322 raise errors.MultipleDebuggeesError(None, debuggees) 323 324 def FindDebuggee(self, pattern=None): 325 """Find the unique debuggee matching the given pattern. 326 327 Args: 328 pattern: A string containing a debuggee ID or a regular expression that 329 matches a single debuggee's name or description. If it matches any 330 debuggee name, the description will not be inspected. 331 Returns: 332 The matching Debuggee. 333 Raises: 334 errors.MultipleDebuggeesError if the pattern matches multiple debuggees. 335 errors.NoDebuggeeError if the pattern matches no debuggees. 336 """ 337 if not pattern: 338 debuggee = self.DefaultDebuggee() 339 log.status.write( 340 'Debug target not specified. Using default target: {0}\n'.format( 341 debuggee.name)) 342 return debuggee 343 344 try: 345 # Look for active debuggees first, since there are usually very 346 # few of them compared to inactive debuggees. 347 all_debuggees = self.ListDebuggees() 348 return self._FilterDebuggeeList(all_debuggees, pattern) 349 except errors.NoDebuggeeError: 350 # Try looking at inactive debuggees 351 pass 352 all_debuggees = self.ListDebuggees(include_inactive=True, 353 include_stale=True) 354 return self._FilterDebuggeeList(all_debuggees, pattern) 355 356 def _FilterDebuggeeList(self, all_debuggees, pattern): 357 """Finds the debuggee which matches the given pattern. 358 359 Args: 360 all_debuggees: A list of debuggees to search. 361 pattern: A string containing a debuggee ID or a regular expression that 362 matches a single debuggee's name or description. If it matches any 363 debuggee name, the description will not be inspected. 364 Returns: 365 The matching Debuggee. 366 Raises: 367 errors.MultipleDebuggeesError if the pattern matches multiple debuggees. 368 errors.NoDebuggeeError if the pattern matches no debuggees. 369 """ 370 if not all_debuggees: 371 raise errors.NoDebuggeeError() 372 373 latest_debuggees = _FilterStaleMinorVersions(all_debuggees) 374 375 # Find all debuggees specified by ID, plus all debuggees which are the 376 # latest minor version when specified by name. 377 debuggees = ([d for d in all_debuggees if d.target_id == pattern] + 378 [d for d in latest_debuggees if pattern == d.name]) 379 if not debuggees: 380 # Try matching as an RE on name or description. Name and description 381 # share common substrings, so filter out duplicates. 382 match_re = re.compile(pattern) 383 debuggees = ( 384 [d for d in latest_debuggees if match_re.search(d.name)] + 385 [d for d in latest_debuggees 386 if d.description and match_re.search(d.description)]) 387 388 if not debuggees: 389 raise errors.NoDebuggeeError(pattern, debuggees=all_debuggees) 390 391 debuggee_ids = set(d.target_id for d in debuggees) 392 if len(debuggee_ids) > 1: 393 raise errors.MultipleDebuggeesError(pattern, debuggees) 394 395 # Just one possible target 396 return debuggees[0] 397 398 def RegisterDebuggee(self, description, uniquifier, agent_version=None): 399 """Register a debuggee with the Cloud Debugger. 400 401 This method is primarily intended to simplify testing, since it registering 402 a debuggee is only a small part of the functionality of a debug agent, and 403 the rest of the API is not supported here. 404 Args: 405 description: A concise description of the debuggee. 406 uniquifier: A string uniquely identifying the debug target. Note that the 407 uniquifier distinguishes between different deployments of a service, 408 not between different replicas of a single deployment. I.e., all 409 replicas of a single deployment should report the same uniquifier. 410 agent_version: A string describing the program registering the debuggee. 411 Defaults to "google.com/gcloud/NNN" where NNN is the gcloud version. 412 Returns: 413 The registered Debuggee. 414 """ 415 if not agent_version: 416 agent_version = self.CLIENT_VERSION 417 request = self._debug_messages.RegisterDebuggeeRequest( 418 debuggee=self._debug_messages.Debuggee( 419 project=self._project, description=description, 420 uniquifier=uniquifier, agentVersion=agent_version)) 421 try: 422 response = self._debug_client.controller_debuggees.Register(request) 423 except apitools_exceptions.HttpError as error: 424 raise errors.UnknownHttpError(error) 425 return Debuggee(response.debuggee) 426 427 428class Debuggee(DebugObject): 429 """Represents a single debuggee.""" 430 431 def __init__(self, message, debug_client=None, debug_messages=None, 432 resource_client=None, resource_messages=None): 433 super(Debuggee, self).__init__( 434 debug_client=debug_client, debug_messages=debug_messages, 435 resource_client=resource_client, resource_messages=resource_messages) 436 self.project = message.project 437 self.agent_version = message.agentVersion 438 self.description = message.description 439 self.ext_source_contexts = message.extSourceContexts 440 self.target_id = message.id 441 self.is_disabled = message.isDisabled 442 self.is_inactive = message.isInactive 443 self.source_contexts = message.sourceContexts 444 self.status = message.status 445 self.target_uniquifier = message.uniquifier 446 self.labels = {} 447 if message.labels: 448 for l in message.labels.additionalProperties: 449 self.labels[l.key] = l.value 450 451 def __eq__(self, other): 452 return (isinstance(other, self.__class__) and 453 self.target_id == other.target_id) 454 455 def __ne__(self, other): 456 return not self.__eq__(other) 457 458 def __repr__(self): 459 return '<id={0}, name={1}{2}>'.format( 460 self.target_id, self.name, ', description={0}'.format(self.description) 461 if self.description else '') 462 463 @property 464 def service(self): 465 return self.labels.get('module', None) 466 467 @property 468 def version(self): 469 return self.labels.get('version', None) 470 471 @property 472 def minorversion(self): 473 return self.labels.get('minorversion', None) 474 475 @property 476 def name(self): 477 service = self.service 478 version = self.version 479 if service or version: 480 return (service or DEFAULT_MODULE) + '-' + (version or DEFAULT_VERSION) 481 return self.description 482 483 def _BreakpointDescription(self, restrict_to_type): 484 if not restrict_to_type: 485 return 'breakpoint' 486 elif restrict_to_type == self.SNAPSHOT_TYPE: 487 return 'snapshot' 488 else: 489 return 'logpoint' 490 491 def GetBreakpoint(self, breakpoint_id): 492 """Gets the details for a breakpoint. 493 494 Args: 495 breakpoint_id: A breakpoint ID. 496 Returns: 497 The full Breakpoint message for the ID. 498 """ 499 request = (self._debug_messages. 500 ClouddebuggerDebuggerDebuggeesBreakpointsGetRequest( 501 breakpointId=breakpoint_id, debuggeeId=self.target_id, 502 clientVersion=self.CLIENT_VERSION)) 503 try: 504 response = self._debug_client.debugger_debuggees_breakpoints.Get(request) 505 except apitools_exceptions.HttpError as error: 506 raise errors.UnknownHttpError(error) 507 return self.AddTargetInfo(response.breakpoint) 508 509 def DeleteBreakpoint(self, breakpoint_id): 510 """Deletes a breakpoint. 511 512 Args: 513 breakpoint_id: A breakpoint ID. 514 """ 515 request = (self._debug_messages. 516 ClouddebuggerDebuggerDebuggeesBreakpointsDeleteRequest( 517 breakpointId=breakpoint_id, debuggeeId=self.target_id, 518 clientVersion=self.CLIENT_VERSION)) 519 try: 520 self._debug_client.debugger_debuggees_breakpoints.Delete(request) 521 except apitools_exceptions.HttpError as error: 522 raise errors.UnknownHttpError(error) 523 524 def ListBreakpoints(self, location_regexp=None, resource_ids=None, 525 include_all_users=False, include_inactive=False, 526 restrict_to_type=None, full_details=False): 527 """Returns all breakpoints matching the given IDs or patterns. 528 529 Lists all breakpoints for this debuggee, and returns every breakpoint 530 where the location field contains the given pattern or the ID is exactly 531 equal to the pattern (there can be at most one breakpoint matching by ID). 532 533 Args: 534 location_regexp: A list of regular expressions to compare against the 535 location ('path:line') of the breakpoints. If both location_regexp and 536 resource_ids are empty or None, all breakpoints will be returned. 537 resource_ids: Zero or more resource IDs in the form expected by the 538 resource parser. These breakpoints will be retrieved regardless 539 of the include_all_users or include_inactive flags 540 include_all_users: If true, search breakpoints created by all users. 541 include_inactive: If true, search breakpoints that are in the final state. 542 This option controls whether regular expressions can match inactive 543 breakpoints. If an object is specified by ID, it will be returned 544 whether or not this flag is set. 545 restrict_to_type: An optional breakpoint type (LOGPOINT_TYPE or 546 SNAPSHOT_TYPE) 547 full_details: If true, issue a GetBreakpoint request for every result to 548 get full details including the call stack and variable table. 549 Returns: 550 A list of all matching breakpoints. 551 Raises: 552 InvalidLocationException if a regular expression is not valid. 553 """ 554 resource_ids = resource_ids or [] 555 location_regexp = location_regexp or [] 556 ids = set( 557 [self._resource_parser.Parse( 558 r, params={'debuggeeId': self.target_id}, 559 collection='clouddebugger.debugger.debuggees.breakpoints').Name() 560 for r in resource_ids]) 561 patterns = [] 562 for r in location_regexp: 563 try: 564 patterns.append(re.compile(r'^(.*/)?(' + r + ')$')) 565 except re.error as e: 566 raise errors.InvalidLocationException( 567 'The location pattern "{0}" is not a valid Python regular ' 568 'expression: {1}'.format(r, e)) 569 570 request = (self._debug_messages. 571 ClouddebuggerDebuggerDebuggeesBreakpointsListRequest( 572 debuggeeId=self.target_id, 573 includeAllUsers=include_all_users, 574 includeInactive=include_inactive or bool(ids), 575 clientVersion=self.CLIENT_VERSION)) 576 try: 577 response = self._debug_client.debugger_debuggees_breakpoints.List(request) 578 except apitools_exceptions.HttpError as error: 579 raise errors.UnknownHttpError(error) 580 if not patterns and not ids: 581 return self._FilteredDictListWithInfo(response.breakpoints, 582 restrict_to_type) 583 584 if include_inactive: 585 # Match everything (including inactive breakpoints) against all ids and 586 # patterns. 587 result = [bp for bp in response.breakpoints 588 if _BreakpointMatchesIdOrRegexp(bp, ids, patterns)] 589 else: 590 # Return everything that is listed by ID, plus every breakpoint that 591 # is not inactive (i.e. isFinalState is false) which matches any pattern. 592 # Breakpoints that are inactive should not be matched against the 593 # patterns. 594 result = [bp for bp in response.breakpoints 595 if _BreakpointMatchesIdOrRegexp( 596 bp, ids, [] if bp.isFinalState else patterns)] 597 # Check if any ids were missing, and fetch them individually. This can 598 # happen if an ID for another user's breakpoint was specified, but the 599 # all_users flag was false. This code will also raise an error for any 600 # missing IDs. 601 missing_ids = ids - set([bp.id for bp in result]) 602 if missing_ids: 603 raise errors.BreakpointNotFoundError( 604 missing_ids, self._BreakpointDescription(restrict_to_type)) 605 606 # Verify that all patterns matched at least one breakpoint. 607 for p in patterns: 608 if not [bp for bp in result 609 if _BreakpointMatchesIdOrRegexp(bp, [], [p])]: 610 raise errors.NoMatchError(self._BreakpointDescription(restrict_to_type), 611 p.pattern) 612 result = self._FilteredDictListWithInfo(result, restrict_to_type) 613 if full_details: 614 def IsCompletedSnapshot(bp): 615 return ((not bp.action or 616 bp.action == self.BreakpointAction(self.SNAPSHOT_TYPE)) and 617 bp.isFinalState and not (bp.status and bp.status.isError)) 618 result = [ 619 self.GetBreakpoint(bp.id) if IsCompletedSnapshot(bp) else bp 620 for bp in result 621 ] 622 return result 623 624 def CreateSnapshot(self, location, condition=None, expressions=None, 625 user_email=None, labels=None): 626 """Creates a "snapshot" breakpoint. 627 628 Args: 629 location: The breakpoint source location, which will be interpreted by 630 the debug agents on the machines running the Debuggee. Usually of the 631 form file:line-number 632 condition: An optional conditional expression in the target's programming 633 language. The snapshot will be taken when the expression is true. 634 expressions: A list of expressions to evaluate when the snapshot is 635 taken. 636 user_email: The email of the user who created the snapshot. 637 labels: A dictionary containing key-value pairs which will be stored 638 with the snapshot definition and reported when the snapshot is queried. 639 Returns: 640 The created Breakpoint message. 641 """ 642 labels_value = None 643 if labels: 644 labels_value = self._debug_messages.Breakpoint.LabelsValue( 645 additionalProperties=[ 646 self._debug_messages.Breakpoint.LabelsValue.AdditionalProperty( 647 key=key, value=value) 648 for key, value in six.iteritems(labels)]) 649 location = self._LocationFromString(location) 650 if not expressions: 651 expressions = [] 652 request = ( 653 self._debug_messages. 654 ClouddebuggerDebuggerDebuggeesBreakpointsSetRequest( 655 debuggeeId=self.target_id, 656 breakpoint=self._debug_messages.Breakpoint( 657 location=location, condition=condition, expressions=expressions, 658 labels=labels_value, userEmail=user_email, 659 action=(self._debug_messages.Breakpoint. 660 ActionValueValuesEnum.CAPTURE)), 661 clientVersion=self.CLIENT_VERSION)) 662 try: 663 response = self._debug_client.debugger_debuggees_breakpoints.Set(request) 664 except apitools_exceptions.HttpError as error: 665 raise errors.UnknownHttpError(error) 666 return self.AddTargetInfo(response.breakpoint) 667 668 def CreateLogpoint(self, location, log_format_string, log_level=None, 669 condition=None, user_email=None, labels=None): 670 """Creates a logpoint in the debuggee. 671 672 Args: 673 location: The breakpoint source location, which will be interpreted by 674 the debug agents on the machines running the Debuggee. Usually of the 675 form file:line-number 676 log_format_string: The message to log, optionally containin {expression}- 677 style formatting. 678 log_level: String (case-insensitive), one of 'info', 'warning', or 679 'error', indicating the log level that should be used for logging. 680 condition: An optional conditional expression in the target's programming 681 language. The snapshot will be taken when the expression is true. 682 user_email: The email of the user who created the snapshot. 683 labels: A dictionary containing key-value pairs which will be stored 684 with the snapshot definition and reported when the snapshot is queried. 685 Returns: 686 The created Breakpoint message. 687 Raises: 688 InvalidLocationException: if location is empty or malformed. 689 InvalidLogFormatException: if log_format is empty or malformed. 690 """ 691 if not location: 692 raise errors.InvalidLocationException( 693 'The location must not be empty.') 694 if not log_format_string: 695 raise errors.InvalidLogFormatException( 696 'The log format string must not be empty.') 697 labels_value = None 698 if labels: 699 labels_value = self._debug_messages.Breakpoint.LabelsValue( 700 additionalProperties=[ 701 self._debug_messages.Breakpoint.LabelsValue.AdditionalProperty( 702 key=key, value=value) 703 for key, value in six.iteritems(labels)]) 704 location = self._LocationFromString(location) 705 if log_level: 706 log_level = ( 707 self._debug_messages.Breakpoint.LogLevelValueValuesEnum( 708 log_level.upper())) 709 log_message_format, expressions = SplitLogExpressions(log_format_string) 710 request = ( 711 self._debug_messages. 712 ClouddebuggerDebuggerDebuggeesBreakpointsSetRequest( 713 debuggeeId=self.target_id, 714 breakpoint=self._debug_messages.Breakpoint( 715 location=location, condition=condition, logLevel=log_level, 716 logMessageFormat=log_message_format, expressions=expressions, 717 labels=labels_value, userEmail=user_email, 718 action=(self._debug_messages.Breakpoint. 719 ActionValueValuesEnum.LOG)), 720 clientVersion=self.CLIENT_VERSION)) 721 try: 722 response = self._debug_client.debugger_debuggees_breakpoints.Set(request) 723 except apitools_exceptions.HttpError as error: 724 raise errors.UnknownHttpError(error) 725 return self.AddTargetInfo(response.breakpoint) 726 727 def _CallGet(self, request): 728 with self._client_lock: 729 return self._debug_client.debugger_debuggees_breakpoints.Get(request) 730 731 def WaitForBreakpointSet(self, breakpoint_id, original_location, timeout=None, 732 retry_ms=500): 733 """Waits for a breakpoint to be set by at least one agent. 734 735 Breakpoint set can be detected in two ways: it can be completed, or the 736 location may change if the breakpoint could not be set at the specified 737 location. A breakpoint may also be set without any change being reported 738 to the server, in which case this function will wait until the timeout 739 is reached. 740 Args: 741 breakpoint_id: A breakpoint ID. 742 original_location: string, the user-specified breakpoint location. If a 743 response has a different location, the function will return immediately. 744 timeout: The number of seconds to wait for completion. 745 retry_ms: Milliseconds to wait betweeen retries. 746 Returns: 747 The Breakpoint message, or None if the breakpoint did not get set before 748 the timeout. 749 """ 750 def MovedOrFinal(r): 751 return ( 752 r.breakpoint.isFinalState or 753 (original_location and 754 original_location != _FormatLocation(r.breakpoint.location))) 755 try: 756 return self.WaitForBreakpoint( 757 breakpoint_id=breakpoint_id, timeout=timeout, retry_ms=retry_ms, 758 completion_test=MovedOrFinal) 759 except apitools_exceptions.HttpError as error: 760 raise errors.UnknownHttpError(error) 761 762 def WaitForBreakpoint(self, breakpoint_id, timeout=None, retry_ms=500, 763 completion_test=None): 764 """Waits for a breakpoint to be completed. 765 766 Args: 767 breakpoint_id: A breakpoint ID. 768 timeout: The number of seconds to wait for completion. 769 retry_ms: Milliseconds to wait betweeen retries. 770 completion_test: A function that accepts a Breakpoint message and 771 returns True if the breakpoint wait is not finished. If not specified, 772 defaults to a function which just checks the isFinalState flag. 773 Returns: 774 The Breakpoint message, or None if the breakpoint did not complete before 775 the timeout, 776 """ 777 if not completion_test: 778 completion_test = lambda r: r.breakpoint.isFinalState 779 retry_if = lambda r, _: not completion_test(r) 780 retryer = retry.Retryer( 781 max_wait_ms=1000*timeout if timeout is not None else None, 782 wait_ceiling_ms=1000) 783 request = (self._debug_messages. 784 ClouddebuggerDebuggerDebuggeesBreakpointsGetRequest( 785 breakpointId=breakpoint_id, debuggeeId=self.target_id, 786 clientVersion=self.CLIENT_VERSION)) 787 try: 788 result = retryer.RetryOnResult(self._CallGet, [request], 789 should_retry_if=retry_if, 790 sleep_ms=retry_ms) 791 except retry.RetryException: 792 # Timeout before the beakpoint was finalized. 793 return None 794 except apitools_exceptions.HttpError as error: 795 raise errors.UnknownHttpError(error) 796 if not completion_test(result): 797 # Termination condition was not met 798 return None 799 return self.AddTargetInfo(result.breakpoint) 800 801 def WaitForMultipleBreakpoints(self, ids, wait_all=False, timeout=None): 802 """Waits for one or more breakpoints to complete. 803 804 Args: 805 ids: A list of breakpoint IDs. 806 wait_all: If True, wait for all breakpoints to complete. Otherwise, wait 807 for any breakpoint to complete. 808 timeout: The number of seconds to wait for completion. 809 Returns: 810 The completed Breakpoint messages, in the order requested. If wait_all was 811 specified and the timeout was reached, the result will still comprise the 812 completed Breakpoints. 813 """ 814 waiter = _BreakpointWaiter(wait_all, timeout) 815 for i in ids: 816 waiter.AddTarget(self, i) 817 results = waiter.Wait() 818 return [results[i] for i in ids if i in results] 819 820 def AddTargetInfo(self, message): 821 """Converts a message into an object with added debuggee information. 822 823 Args: 824 message: A message returned from a debug API call. 825 Returns: 826 An object including the fields of the original object plus the following 827 fields: project, target_uniquifier, and target_id. 828 """ 829 result = _MessageDict(message, hidden_fields={ 830 'project': self.project, 831 'target_uniquifier': self.target_uniquifier, 832 'target_id': self.target_id, 833 'service': self.service, 834 'version': self.version}) 835 # Restore some default values if they were stripped 836 if (message.action == 837 self._debug_messages.Breakpoint.ActionValueValuesEnum.LOG and 838 not message.logLevel): 839 result['logLevel'] = ( 840 self._debug_messages.Breakpoint.LogLevelValueValuesEnum.INFO) 841 842 if message.isFinalState is None: 843 result['isFinalState'] = False 844 845 # Reformat a few fields for readability 846 if message.location: 847 result['location'] = _FormatLocation(message.location) 848 if message.logMessageFormat: 849 result['logMessageFormat'] = MergeLogExpressions(message.logMessageFormat, 850 message.expressions) 851 result.HideExistingField('expressions') 852 853 if not message.status or not message.status.isError: 854 if message.action == self.BreakpointAction(self.LOGPOINT_TYPE): 855 # We can only generate view URLs for GAE, since there's not a standard 856 # way to view them in GCE. Use the presence of minorversion as an 857 # indicator that it's GAE. 858 if self.minorversion: 859 result['logQuery'] = LogQueryV2String(result) 860 result['logViewUrl'] = LogViewUrl(result) 861 else: 862 result['consoleViewUrl'] = DebugViewUrl(result) 863 864 return result 865 866 def _LocationFromString(self, location): 867 """Converts a file:line location string into a SourceLocation. 868 869 Args: 870 location: A string of the form file:line. 871 Returns: 872 The corresponding SourceLocation message. 873 Raises: 874 InvalidLocationException: if the line is not of the form path:line 875 """ 876 components = location.split(':') 877 if len(components) != 2: 878 raise errors.InvalidLocationException( 879 'Location must be of the form "path:line"') 880 try: 881 return self._debug_messages.SourceLocation(path=components[0], 882 line=int(components[1])) 883 except ValueError: 884 raise errors.InvalidLocationException( 885 'Location must be of the form "path:line", where "line" must be an ' 886 'integer.') 887 888 def _FilteredDictListWithInfo(self, result, restrict_to_type): 889 """Filters a result list to contain only breakpoints of the given type. 890 891 Args: 892 result: A list of breakpoint messages, to be filtered. 893 restrict_to_type: An optional breakpoint type. If None, no filtering 894 will be done. 895 Returns: 896 The filtered result, converted to equivalent dicts with debug info fields 897 added. 898 """ 899 return [self.AddTargetInfo(r) for r in result 900 if not restrict_to_type 901 or r.action == self.BreakpointAction(restrict_to_type) 902 or (not r.action and restrict_to_type == self.SNAPSHOT_TYPE)] 903 904 905class _BreakpointWaiter(object): 906 """Waits for multiple breakpoints. 907 908 Attributes: 909 _result_lock: Lock for modifications to all fields 910 _done: Flag to indicate that the wait condition is satisfied and wait 911 should stop even if some threads are not finished. 912 _threads: The list of active threads 913 _results: The set of completed breakpoints. 914 _failures: All exceptions which caused any thread to stop waiting. 915 _wait_all: If true, wait for all breakpoints to complete, else wait for 916 any breakpoint to complete. Controls whether to set _done after any 917 breakpoint completes. 918 _timeout: Mazimum time (in ms) to wait for breakpoints to complete. 919 """ 920 921 def __init__(self, wait_all, timeout): 922 self._result_lock = threading.Lock() 923 self._done = False 924 self._threads = [] 925 self._results = {} 926 self._failures = [] 927 self._wait_all = wait_all 928 self._timeout = timeout 929 930 def _IsComplete(self, response): 931 if response.breakpoint.isFinalState: 932 return True 933 with self._result_lock: 934 return self._done 935 936 def _WaitForOne(self, debuggee, breakpoint_id): 937 try: 938 breakpoint = debuggee.WaitForBreakpoint( 939 breakpoint_id, timeout=self._timeout, 940 completion_test=self._IsComplete) 941 if not breakpoint: 942 # Breakpoint never completed (i.e. timeout) 943 with self._result_lock: 944 if not self._wait_all: 945 self._done = True 946 return 947 if breakpoint.isFinalState: 948 with self._result_lock: 949 self._results[breakpoint_id] = breakpoint 950 if not self._wait_all: 951 self._done = True 952 except errors.DebugError as e: 953 with self._result_lock: 954 self._failures.append(e) 955 self._done = True 956 957 def AddTarget(self, debuggee, breakpoint_id): 958 self._threads.append( 959 threading.Thread(target=self._WaitForOne, 960 args=(debuggee, breakpoint_id))) 961 962 def Wait(self): 963 for t in self._threads: 964 t.start() 965 for t in self._threads: 966 t.join() 967 if self._failures: 968 # Just raise the first exception we handled 969 raise self._failures[0] 970 return self._results 971 972 973def _FormatLocation(location): 974 if not location: 975 return None 976 return '{0}:{1}'.format(location.path, location.line) 977 978 979def _BreakpointMatchesIdOrRegexp(breakpoint, ids, patterns): 980 """Check if a breakpoint matches any of the given IDs or regexps. 981 982 Args: 983 breakpoint: Any _debug_messages.Breakpoint message object. 984 ids: A set of strings to search for exact matches on breakpoint ID. 985 patterns: A list of regular expressions to match against the file:line 986 location of the breakpoint. 987 Returns: 988 True if the breakpoint matches any ID or pattern. 989 """ 990 if breakpoint.id in ids: 991 return True 992 if not breakpoint.location: 993 return False 994 location = _FormatLocation(breakpoint.location) 995 for p in patterns: 996 if p.match(location): 997 return True 998 return False 999 1000 1001def _FilterStaleMinorVersions(debuggees): 1002 """Filter out any debugees referring to a stale minor version. 1003 1004 Args: 1005 debuggees: A list of Debuggee objects. 1006 Returns: 1007 A filtered list containing only the debuggees denoting the most recent 1008 minor version with the given name. If any debuggee with a given name does 1009 not have a 'minorversion' label, the resulting list will contain all 1010 debuggees with that name. 1011 """ 1012 # First group by name 1013 byname = {} 1014 for debuggee in debuggees: 1015 if debuggee.name in byname: 1016 byname[debuggee.name].append(debuggee) 1017 else: 1018 byname[debuggee.name] = [debuggee] 1019 # Now look at each list for a given name, choosing only the latest 1020 # version. 1021 result = [] 1022 for name_list in byname.values(): 1023 latest = _FindLatestMinorVersion(name_list) 1024 if latest: 1025 result.append(latest) 1026 else: 1027 result.extend(name_list) 1028 return result 1029 1030 1031def _FindLatestMinorVersion(debuggees): 1032 """Given a list of debuggees, find the one with the highest minor version. 1033 1034 Args: 1035 debuggees: A list of Debuggee objects. 1036 Returns: 1037 If all debuggees have the same name, return the one with the highest 1038 integer value in its 'minorversion' label. If any member of the list does 1039 not have a minor version, or if elements of the list have different 1040 names, returns None. 1041 """ 1042 if not debuggees: 1043 return None 1044 best = None 1045 best_version = None 1046 name = None 1047 for d in debuggees: 1048 if not name: 1049 name = d.name 1050 elif name != d.name: 1051 return None 1052 minor_version = d.labels.get('minorversion', 0) 1053 if not minor_version: 1054 return None 1055 try: 1056 minor_version = int(minor_version) 1057 if not best_version or minor_version > best_version: 1058 best_version = minor_version 1059 best = d 1060 except ValueError: 1061 # Got a bogus minor version. We can't determine which is best. 1062 return None 1063 return best 1064 1065 1066class _MessageDict(dict): 1067 """An extensible wrapper around message data. 1068 1069 Fields can be added as dictionary items and retrieved as attributes. 1070 """ 1071 1072 def __init__(self, message, hidden_fields=None): 1073 super(_MessageDict, self).__init__() 1074 self._orig_type = type(message).__name__ 1075 if hidden_fields: 1076 self._hidden_fields = hidden_fields 1077 else: 1078 self._hidden_fields = {} 1079 for field in message.all_fields(): 1080 value = getattr(message, field.name) 1081 if not value: 1082 self._hidden_fields[field.name] = value 1083 else: 1084 self[field.name] = value 1085 1086 def __getattr__(self, attr): 1087 if attr in self: 1088 return self[attr] 1089 if attr in self._hidden_fields: 1090 return self._hidden_fields[attr] 1091 raise AttributeError('Type "{0}" does not have attribute "{1}"'.format( 1092 self._orig_type, attr)) 1093 1094 def HideExistingField(self, field_name): 1095 if field_name in self._hidden_fields: 1096 return 1097 self._hidden_fields[field_name] = self.pop(field_name, None) 1098