File: //snap/google-cloud-cli/396/lib/googlecloudsdk/api_lib/debug/debug.py
# -*- coding: utf-8 -*- #
# Copyright 2016 Google LLC. 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.
"""Debug apis layer."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
import threading
from apitools.base.py import exceptions as apitools_exceptions
from googlecloudsdk.api_lib.debug import errors
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.core import config
from googlecloudsdk.core import log
from googlecloudsdk.core import resources
from googlecloudsdk.core.util import retry
import six
from six.moves import urllib
# Names for default module and version. In App Engine, the default module and
# version don't report explicit names to the debugger, so use these strings
# instead when displaying the target name. Note that this code assumes there
# will not be a non-default version or module explicitly named 'default', since
# that would result in a naming conflict between the actual default and the
# one named 'default'.
DEFAULT_MODULE = 'default'
DEFAULT_VERSION = 'default'
def SplitLogExpressions(format_string):
"""Extracts {expression} substrings into a separate array.
Each substring of the form {expression} will be extracted into an array, and
each {expression} substring will be replaced with $N, where N is the index
of the extraced expression in the array. Any '$' sequence outside an
expression will be escaped with '$$'.
For example, given the input:
'a={a}, b={b}'
The return value would be:
('a=$0, b=$1', ['a', 'b'])
Args:
format_string: The string to process.
Returns:
string, [string] - The new format string and the array of expressions.
Raises:
InvalidLogFormatException: if the string has unbalanced braces.
"""
expressions = []
log_format = ''
current_expression = ''
brace_count = 0
need_separator = False
for c in format_string:
if need_separator and c.isdigit():
log_format += ' '
need_separator = False
if c == '{':
if brace_count:
# Nested braces
current_expression += c
else:
# New expression
current_expression = ''
brace_count += 1
elif not brace_count:
if c == '}':
# Unbalanced left brace.
raise errors.InvalidLogFormatException(
'There are too many "}" characters in the log format string')
elif c == '$':
# Escape '$'
log_format += '$$'
else:
# Not in or starting an expression.
log_format += c
else:
# Currently reading an expression.
if c != '}':
current_expression += c
continue
brace_count -= 1
if brace_count == 0:
# Finish processing the expression
if current_expression in expressions:
i = expressions.index(current_expression)
else:
i = len(expressions)
expressions.append(current_expression)
log_format += '${0}'.format(i)
# If the next character is a digit, we need an extra space to prevent
# the agent from combining the positional argument with the subsequent
# digits.
need_separator = True
else:
# Closing a nested brace
current_expression += c
if brace_count:
# Unbalanced left brace.
raise errors.InvalidLogFormatException(
'There are too many "{" characters in the log format string')
return log_format, expressions
def MergeLogExpressions(log_format, expressions):
"""Replaces each $N substring with the corresponding {expression}.
This function is intended for reconstructing an input expression string that
has been split using SplitLogExpressions. It is not intended for substituting
the expression results at log time.
Args:
log_format: A string containing 0 or more $N substrings, where N is any
valid index into the expressions array. Each such substring will be
replaced by '{expression}', where "expression" is expressions[N].
expressions: The expressions to substitute into the format string.
Returns:
The combined string.
"""
def GetExpression(m):
try:
return '{{{0}}}'.format(expressions[int(m.group(0)[1:])])
except IndexError:
return m.group(0)
parts = log_format.split('$$')
return '$'.join(re.sub(r'\$\d+', GetExpression, part) for part in parts)
def DebugViewUrl(breakpoint):
"""Returns a URL to view a breakpoint in the browser.
Given a breakpoint, this transform will return a URL which will open the
snapshot's location in a debug view pointing at the snapshot.
Args:
breakpoint: A breakpoint object with added information on project and
debug target.
Returns:
The URL for the breakpoint.
"""
debug_view_url = 'https://console.cloud.google.com/debug/fromgcloud?'
data = [
('project', breakpoint.project),
('dbgee', breakpoint.target_id),
('bp', breakpoint.id)
]
return debug_view_url + urllib.parse.urlencode(data)
def LogQueryV2String(breakpoint, separator=' '):
"""Returns an advanced log query string for use with gcloud logging read.
Args:
breakpoint: A breakpoint object with added information on project, service,
and debug target.
separator: A string to append between conditions
Returns:
A log query suitable for use with gcloud logging read.
Raises:
InvalidLogFormatException if the breakpoint has an invalid log expression.
"""
query = (
'resource.type=gae_app{sep}'
'logName:request_log{sep}'
'resource.labels.module_id="{service}"{sep}'
'resource.labels.version_id="{version}"{sep}'
'severity={logLevel}').format(
service=breakpoint.service, version=breakpoint.version,
logLevel=breakpoint.logLevel or 'INFO', sep=separator)
if breakpoint.logMessageFormat:
# Search for all of the non-expression components of the message.
# The re.sub converts the format to a series of quoted strings.
query += '{sep}"{text}"'.format(
text=re.sub(r'\$([0-9]+)', r'" "',
SplitLogExpressions(breakpoint.logMessageFormat)[0]),
sep=separator)
return query
def LogViewUrl(breakpoint):
"""Returns a URL to view the output for a logpoint.
Given a breakpoint in an appengine service, this transform will return a URL
which will open the log viewer to the request log for the service.
Args:
breakpoint: A breakpoint object with added information on project, service,
debug target, and logQuery.
Returns:
The URL for the appropriate logs.
"""
debug_view_url = 'https://console.cloud.google.com/logs?'
data = [
('project', breakpoint.project),
('advancedFilter', LogQueryV2String(breakpoint, separator='\n') + '\n')
]
return debug_view_url + urllib.parse.urlencode(data)
class DebugObject(object):
"""Base class for debug api wrappers."""
# Lock for remote calls in routines which might be multithreaded. Client
# connections are not thread-safe. Currently, only WaitForBreakpoint can
# be called from multiple threads.
_client_lock = threading.Lock()
# Breakpoint type name constants
SNAPSHOT_TYPE = 'SNAPSHOT'
LOGPOINT_TYPE = 'LOGPOINT'
def BreakpointAction(self, type_name):
if type_name == self.SNAPSHOT_TYPE:
return self._debug_messages.Breakpoint.ActionValueValuesEnum.CAPTURE
if type_name == self.LOGPOINT_TYPE:
return self._debug_messages.Breakpoint.ActionValueValuesEnum.LOG
raise errors.InvalidBreakpointTypeError(type_name)
CLIENT_VERSION = 'google.com/gcloud/{0}'.format(config.CLOUD_SDK_VERSION)
def __init__(self, debug_client=None, debug_messages=None,
resource_client=None, resource_messages=None):
"""Sets up class with instantiated api client."""
self._debug_client = (
debug_client or apis.GetClientInstance('clouddebugger', 'v2'))
self._debug_messages = (
debug_messages or apis.GetMessagesModule('clouddebugger', 'v2'))
self._resource_client = (
resource_client or
apis.GetClientInstance('cloudresourcemanager', 'v1beta1'))
self._resource_messages = (
resource_messages or
apis.GetMessagesModule('cloudresourcemanager', 'v1beta1'))
self._resource_parser = resources.REGISTRY.Clone()
self._resource_parser.RegisterApiByName('clouddebugger', 'v2')
class Debugger(DebugObject):
"""Abstracts Cloud Debugger service for a project."""
def __init__(self, project, debug_client=None, debug_messages=None,
resource_client=None, resource_messages=None):
super(Debugger, self).__init__(
debug_client=debug_client, debug_messages=debug_messages,
resource_client=resource_client, resource_messages=resource_messages)
self._project = project
def ListDebuggees(self, include_inactive=False, include_stale=False):
"""Lists all debug targets registered with the debug service.
Args:
include_inactive: If true, also include debuggees that are not currently
running.
include_stale: If false, filter out any debuggees that refer to
stale minor versions. A debugge represents a stale minor version if it
meets the following criteria:
1. It has a minorversion label.
2. All other debuggees with the same name (i.e., all debuggees with
the same module and version, in the case of app engine) have a
minorversion label.
3. The minorversion value for the debuggee is less than the
minorversion value for at least one other debuggee with the same
name.
Returns:
[Debuggee] A list of debuggees.
"""
request = self._debug_messages.ClouddebuggerDebuggerDebuggeesListRequest(
project=self._project, includeInactive=include_inactive,
clientVersion=self.CLIENT_VERSION)
try:
response = self._debug_client.debugger_debuggees.List(request)
except apitools_exceptions.HttpError as error:
raise errors.UnknownHttpError(error)
result = [Debuggee(debuggee) for debuggee in response.debuggees]
if not include_stale:
return _FilterStaleMinorVersions(result)
return result
def DefaultDebuggee(self):
"""Find the default debuggee.
Returns:
The default debug target, which is either the only target available
or the latest minor version of the application, if all targets have the
same module and version.
Raises:
errors.NoDebuggeeError if no debuggee was found.
errors.MultipleDebuggeesError if there is not a unique default.
"""
debuggees = self.ListDebuggees()
if len(debuggees) == 1:
# Just one possible target
return debuggees[0]
if not debuggees:
raise errors.NoDebuggeeError()
# More than one module or version. Can't determine the default target.
raise errors.MultipleDebuggeesError(None, debuggees)
def FindDebuggee(self, pattern=None):
"""Find the unique debuggee matching the given pattern.
Args:
pattern: A string containing a debuggee ID or a regular expression that
matches a single debuggee's name or description. If it matches any
debuggee name, the description will not be inspected.
Returns:
The matching Debuggee.
Raises:
errors.MultipleDebuggeesError if the pattern matches multiple debuggees.
errors.NoDebuggeeError if the pattern matches no debuggees.
"""
if not pattern:
debuggee = self.DefaultDebuggee()
log.status.write(
'Debug target not specified. Using default target: {0}\n'.format(
debuggee.name))
return debuggee
try:
# Look for active debuggees first, since there are usually very
# few of them compared to inactive debuggees.
all_debuggees = self.ListDebuggees()
return self._FilterDebuggeeList(all_debuggees, pattern)
except errors.NoDebuggeeError:
# Try looking at inactive debuggees
pass
all_debuggees = self.ListDebuggees(include_inactive=True,
include_stale=True)
return self._FilterDebuggeeList(all_debuggees, pattern)
def _FilterDebuggeeList(self, all_debuggees, pattern):
"""Finds the debuggee which matches the given pattern.
Args:
all_debuggees: A list of debuggees to search.
pattern: A string containing a debuggee ID or a regular expression that
matches a single debuggee's name or description. If it matches any
debuggee name, the description will not be inspected.
Returns:
The matching Debuggee.
Raises:
errors.MultipleDebuggeesError if the pattern matches multiple debuggees.
errors.NoDebuggeeError if the pattern matches no debuggees.
"""
if not all_debuggees:
raise errors.NoDebuggeeError()
latest_debuggees = _FilterStaleMinorVersions(all_debuggees)
# Find all debuggees specified by ID, plus all debuggees which are the
# latest minor version when specified by name.
debuggees = ([d for d in all_debuggees if d.target_id == pattern] +
[d for d in latest_debuggees if pattern == d.name])
if not debuggees:
# Try matching as an RE on name or description. Name and description
# share common substrings, so filter out duplicates.
match_re = re.compile(pattern)
debuggees = (
[d for d in latest_debuggees if match_re.search(d.name)] +
[d for d in latest_debuggees
if d.description and match_re.search(d.description)])
if not debuggees:
raise errors.NoDebuggeeError(pattern, debuggees=all_debuggees)
debuggee_ids = set(d.target_id for d in debuggees)
if len(debuggee_ids) > 1:
raise errors.MultipleDebuggeesError(pattern, debuggees)
# Just one possible target
return debuggees[0]
def RegisterDebuggee(self, description, uniquifier, agent_version=None):
"""Register a debuggee with the Cloud Debugger.
This method is primarily intended to simplify testing, since it registering
a debuggee is only a small part of the functionality of a debug agent, and
the rest of the API is not supported here.
Args:
description: A concise description of the debuggee.
uniquifier: A string uniquely identifying the debug target. Note that the
uniquifier distinguishes between different deployments of a service,
not between different replicas of a single deployment. I.e., all
replicas of a single deployment should report the same uniquifier.
agent_version: A string describing the program registering the debuggee.
Defaults to "google.com/gcloud/NNN" where NNN is the gcloud version.
Returns:
The registered Debuggee.
"""
if not agent_version:
agent_version = self.CLIENT_VERSION
request = self._debug_messages.RegisterDebuggeeRequest(
debuggee=self._debug_messages.Debuggee(
project=self._project, description=description,
uniquifier=uniquifier, agentVersion=agent_version))
try:
response = self._debug_client.controller_debuggees.Register(request)
except apitools_exceptions.HttpError as error:
raise errors.UnknownHttpError(error)
return Debuggee(response.debuggee)
class Debuggee(DebugObject):
"""Represents a single debuggee."""
def __init__(self, message, debug_client=None, debug_messages=None,
resource_client=None, resource_messages=None):
super(Debuggee, self).__init__(
debug_client=debug_client, debug_messages=debug_messages,
resource_client=resource_client, resource_messages=resource_messages)
self.project = message.project
self.agent_version = message.agentVersion
self.description = message.description
self.ext_source_contexts = message.extSourceContexts
self.target_id = message.id
self.is_disabled = message.isDisabled
self.is_inactive = message.isInactive
self.source_contexts = message.sourceContexts
self.status = message.status
self.target_uniquifier = message.uniquifier
self.labels = {}
if message.labels:
for l in message.labels.additionalProperties:
self.labels[l.key] = l.value
def __eq__(self, other):
return (isinstance(other, self.__class__) and
self.target_id == other.target_id)
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return '<id={0}, name={1}{2}>'.format(
self.target_id, self.name, ', description={0}'.format(self.description)
if self.description else '')
@property
def service(self):
return self.labels.get('module', None)
@property
def version(self):
return self.labels.get('version', None)
@property
def minorversion(self):
return self.labels.get('minorversion', None)
@property
def name(self):
service = self.service
version = self.version
if service or version:
return (service or DEFAULT_MODULE) + '-' + (version or DEFAULT_VERSION)
return self.description
def _BreakpointDescription(self, restrict_to_type):
if not restrict_to_type:
return 'breakpoint'
elif restrict_to_type == self.SNAPSHOT_TYPE:
return 'snapshot'
else:
return 'logpoint'
def GetBreakpoint(self, breakpoint_id):
"""Gets the details for a breakpoint.
Args:
breakpoint_id: A breakpoint ID.
Returns:
The full Breakpoint message for the ID.
"""
request = (self._debug_messages.
ClouddebuggerDebuggerDebuggeesBreakpointsGetRequest(
breakpointId=breakpoint_id, debuggeeId=self.target_id,
clientVersion=self.CLIENT_VERSION))
try:
response = self._debug_client.debugger_debuggees_breakpoints.Get(request)
except apitools_exceptions.HttpError as error:
raise errors.UnknownHttpError(error)
return self.AddTargetInfo(response.breakpoint)
def DeleteBreakpoint(self, breakpoint_id):
"""Deletes a breakpoint.
Args:
breakpoint_id: A breakpoint ID.
"""
request = (self._debug_messages.
ClouddebuggerDebuggerDebuggeesBreakpointsDeleteRequest(
breakpointId=breakpoint_id, debuggeeId=self.target_id,
clientVersion=self.CLIENT_VERSION))
try:
self._debug_client.debugger_debuggees_breakpoints.Delete(request)
except apitools_exceptions.HttpError as error:
raise errors.UnknownHttpError(error)
def ListBreakpoints(self, location_regexp=None, resource_ids=None,
include_all_users=False, include_inactive=False,
restrict_to_type=None, full_details=False):
"""Returns all breakpoints matching the given IDs or patterns.
Lists all breakpoints for this debuggee, and returns every breakpoint
where the location field contains the given pattern or the ID is exactly
equal to the pattern (there can be at most one breakpoint matching by ID).
Args:
location_regexp: A list of regular expressions to compare against the
location ('path:line') of the breakpoints. If both location_regexp and
resource_ids are empty or None, all breakpoints will be returned.
resource_ids: Zero or more resource IDs in the form expected by the
resource parser. These breakpoints will be retrieved regardless
of the include_all_users or include_inactive flags
include_all_users: If true, search breakpoints created by all users.
include_inactive: If true, search breakpoints that are in the final state.
This option controls whether regular expressions can match inactive
breakpoints. If an object is specified by ID, it will be returned
whether or not this flag is set.
restrict_to_type: An optional breakpoint type (LOGPOINT_TYPE or
SNAPSHOT_TYPE)
full_details: If true, issue a GetBreakpoint request for every result to
get full details including the call stack and variable table.
Returns:
A list of all matching breakpoints.
Raises:
InvalidLocationException if a regular expression is not valid.
"""
resource_ids = resource_ids or []
location_regexp = location_regexp or []
ids = set(
[self._resource_parser.Parse(
r, params={'debuggeeId': self.target_id},
collection='clouddebugger.debugger.debuggees.breakpoints').Name()
for r in resource_ids])
patterns = []
for r in location_regexp:
try:
patterns.append(re.compile(r'^(.*/)?(' + r + ')$'))
except re.error as e:
raise errors.InvalidLocationException(
'The location pattern "{0}" is not a valid Python regular '
'expression: {1}'.format(r, e))
request = (self._debug_messages.
ClouddebuggerDebuggerDebuggeesBreakpointsListRequest(
debuggeeId=self.target_id,
includeAllUsers=include_all_users,
includeInactive=include_inactive or bool(ids),
clientVersion=self.CLIENT_VERSION))
try:
response = self._debug_client.debugger_debuggees_breakpoints.List(request)
except apitools_exceptions.HttpError as error:
raise errors.UnknownHttpError(error)
if not patterns and not ids:
return self._FilteredDictListWithInfo(response.breakpoints,
restrict_to_type)
if include_inactive:
# Match everything (including inactive breakpoints) against all ids and
# patterns.
result = [bp for bp in response.breakpoints
if _BreakpointMatchesIdOrRegexp(bp, ids, patterns)]
else:
# Return everything that is listed by ID, plus every breakpoint that
# is not inactive (i.e. isFinalState is false) which matches any pattern.
# Breakpoints that are inactive should not be matched against the
# patterns.
result = [bp for bp in response.breakpoints
if _BreakpointMatchesIdOrRegexp(
bp, ids, [] if bp.isFinalState else patterns)]
# Check if any ids were missing, and fetch them individually. This can
# happen if an ID for another user's breakpoint was specified, but the
# all_users flag was false. This code will also raise an error for any
# missing IDs.
missing_ids = ids - set([bp.id for bp in result])
if missing_ids:
raise errors.BreakpointNotFoundError(
missing_ids, self._BreakpointDescription(restrict_to_type))
# Verify that all patterns matched at least one breakpoint.
for p in patterns:
if not [bp for bp in result
if _BreakpointMatchesIdOrRegexp(bp, [], [p])]:
raise errors.NoMatchError(self._BreakpointDescription(restrict_to_type),
p.pattern)
result = self._FilteredDictListWithInfo(result, restrict_to_type)
if full_details:
def IsCompletedSnapshot(bp):
return ((not bp.action or
bp.action == self.BreakpointAction(self.SNAPSHOT_TYPE)) and
bp.isFinalState and not (bp.status and bp.status.isError))
result = [
self.GetBreakpoint(bp.id) if IsCompletedSnapshot(bp) else bp
for bp in result
]
return result
def CreateSnapshot(self, location, condition=None, expressions=None,
user_email=None, labels=None):
"""Creates a "snapshot" breakpoint.
Args:
location: The breakpoint source location, which will be interpreted by
the debug agents on the machines running the Debuggee. Usually of the
form file:line-number
condition: An optional conditional expression in the target's programming
language. The snapshot will be taken when the expression is true.
expressions: A list of expressions to evaluate when the snapshot is
taken.
user_email: The email of the user who created the snapshot.
labels: A dictionary containing key-value pairs which will be stored
with the snapshot definition and reported when the snapshot is queried.
Returns:
The created Breakpoint message.
"""
labels_value = None
if labels:
labels_value = self._debug_messages.Breakpoint.LabelsValue(
additionalProperties=[
self._debug_messages.Breakpoint.LabelsValue.AdditionalProperty(
key=key, value=value)
for key, value in six.iteritems(labels)])
location = self._LocationFromString(location)
if not expressions:
expressions = []
request = (
self._debug_messages.
ClouddebuggerDebuggerDebuggeesBreakpointsSetRequest(
debuggeeId=self.target_id,
breakpoint=self._debug_messages.Breakpoint(
location=location, condition=condition, expressions=expressions,
labels=labels_value, userEmail=user_email,
action=(self._debug_messages.Breakpoint.
ActionValueValuesEnum.CAPTURE)),
clientVersion=self.CLIENT_VERSION))
try:
response = self._debug_client.debugger_debuggees_breakpoints.Set(request)
except apitools_exceptions.HttpError as error:
raise errors.UnknownHttpError(error)
return self.AddTargetInfo(response.breakpoint)
def CreateLogpoint(self, location, log_format_string, log_level=None,
condition=None, user_email=None, labels=None):
"""Creates a logpoint in the debuggee.
Args:
location: The breakpoint source location, which will be interpreted by
the debug agents on the machines running the Debuggee. Usually of the
form file:line-number
log_format_string: The message to log, optionally containin {expression}-
style formatting.
log_level: String (case-insensitive), one of 'info', 'warning', or
'error', indicating the log level that should be used for logging.
condition: An optional conditional expression in the target's programming
language. The snapshot will be taken when the expression is true.
user_email: The email of the user who created the snapshot.
labels: A dictionary containing key-value pairs which will be stored
with the snapshot definition and reported when the snapshot is queried.
Returns:
The created Breakpoint message.
Raises:
InvalidLocationException: if location is empty or malformed.
InvalidLogFormatException: if log_format is empty or malformed.
"""
if not location:
raise errors.InvalidLocationException(
'The location must not be empty.')
if not log_format_string:
raise errors.InvalidLogFormatException(
'The log format string must not be empty.')
labels_value = None
if labels:
labels_value = self._debug_messages.Breakpoint.LabelsValue(
additionalProperties=[
self._debug_messages.Breakpoint.LabelsValue.AdditionalProperty(
key=key, value=value)
for key, value in six.iteritems(labels)])
location = self._LocationFromString(location)
if log_level:
log_level = (
self._debug_messages.Breakpoint.LogLevelValueValuesEnum(
log_level.upper()))
log_message_format, expressions = SplitLogExpressions(log_format_string)
request = (
self._debug_messages.
ClouddebuggerDebuggerDebuggeesBreakpointsSetRequest(
debuggeeId=self.target_id,
breakpoint=self._debug_messages.Breakpoint(
location=location, condition=condition, logLevel=log_level,
logMessageFormat=log_message_format, expressions=expressions,
labels=labels_value, userEmail=user_email,
action=(self._debug_messages.Breakpoint.
ActionValueValuesEnum.LOG)),
clientVersion=self.CLIENT_VERSION))
try:
response = self._debug_client.debugger_debuggees_breakpoints.Set(request)
except apitools_exceptions.HttpError as error:
raise errors.UnknownHttpError(error)
return self.AddTargetInfo(response.breakpoint)
def _CallGet(self, request):
with self._client_lock:
return self._debug_client.debugger_debuggees_breakpoints.Get(request)
def WaitForBreakpointSet(self, breakpoint_id, original_location, timeout=None,
retry_ms=500):
"""Waits for a breakpoint to be set by at least one agent.
Breakpoint set can be detected in two ways: it can be completed, or the
location may change if the breakpoint could not be set at the specified
location. A breakpoint may also be set without any change being reported
to the server, in which case this function will wait until the timeout
is reached.
Args:
breakpoint_id: A breakpoint ID.
original_location: string, the user-specified breakpoint location. If a
response has a different location, the function will return immediately.
timeout: The number of seconds to wait for completion.
retry_ms: Milliseconds to wait betweeen retries.
Returns:
The Breakpoint message, or None if the breakpoint did not get set before
the timeout.
"""
def MovedOrFinal(r):
return (
r.breakpoint.isFinalState or
(original_location and
original_location != _FormatLocation(r.breakpoint.location)))
try:
return self.WaitForBreakpoint(
breakpoint_id=breakpoint_id, timeout=timeout, retry_ms=retry_ms,
completion_test=MovedOrFinal)
except apitools_exceptions.HttpError as error:
raise errors.UnknownHttpError(error)
def WaitForBreakpoint(self, breakpoint_id, timeout=None, retry_ms=500,
completion_test=None):
"""Waits for a breakpoint to be completed.
Args:
breakpoint_id: A breakpoint ID.
timeout: The number of seconds to wait for completion.
retry_ms: Milliseconds to wait betweeen retries.
completion_test: A function that accepts a Breakpoint message and
returns True if the breakpoint wait is not finished. If not specified,
defaults to a function which just checks the isFinalState flag.
Returns:
The Breakpoint message, or None if the breakpoint did not complete before
the timeout,
"""
if not completion_test:
completion_test = lambda r: r.breakpoint.isFinalState
retry_if = lambda r, _: not completion_test(r)
retryer = retry.Retryer(
max_wait_ms=1000*timeout if timeout is not None else None,
wait_ceiling_ms=1000)
request = (self._debug_messages.
ClouddebuggerDebuggerDebuggeesBreakpointsGetRequest(
breakpointId=breakpoint_id, debuggeeId=self.target_id,
clientVersion=self.CLIENT_VERSION))
try:
result = retryer.RetryOnResult(self._CallGet, [request],
should_retry_if=retry_if,
sleep_ms=retry_ms)
except retry.RetryException:
# Timeout before the beakpoint was finalized.
return None
except apitools_exceptions.HttpError as error:
raise errors.UnknownHttpError(error)
if not completion_test(result):
# Termination condition was not met
return None
return self.AddTargetInfo(result.breakpoint)
def WaitForMultipleBreakpoints(self, ids, wait_all=False, timeout=None):
"""Waits for one or more breakpoints to complete.
Args:
ids: A list of breakpoint IDs.
wait_all: If True, wait for all breakpoints to complete. Otherwise, wait
for any breakpoint to complete.
timeout: The number of seconds to wait for completion.
Returns:
The completed Breakpoint messages, in the order requested. If wait_all was
specified and the timeout was reached, the result will still comprise the
completed Breakpoints.
"""
waiter = _BreakpointWaiter(wait_all, timeout)
for i in ids:
waiter.AddTarget(self, i)
results = waiter.Wait()
return [results[i] for i in ids if i in results]
def AddTargetInfo(self, message):
"""Converts a message into an object with added debuggee information.
Args:
message: A message returned from a debug API call.
Returns:
An object including the fields of the original object plus the following
fields: project, target_uniquifier, and target_id.
"""
result = _MessageDict(message, hidden_fields={
'project': self.project,
'target_uniquifier': self.target_uniquifier,
'target_id': self.target_id,
'service': self.service,
'version': self.version})
# Restore some default values if they were stripped
if (message.action ==
self._debug_messages.Breakpoint.ActionValueValuesEnum.LOG and
not message.logLevel):
result['logLevel'] = (
self._debug_messages.Breakpoint.LogLevelValueValuesEnum.INFO)
if message.isFinalState is None:
result['isFinalState'] = False
# Reformat a few fields for readability
if message.location:
result['location'] = _FormatLocation(message.location)
if message.logMessageFormat:
result['logMessageFormat'] = MergeLogExpressions(message.logMessageFormat,
message.expressions)
result.HideExistingField('expressions')
if not message.status or not message.status.isError:
if message.action == self.BreakpointAction(self.LOGPOINT_TYPE):
# We can only generate view URLs for GAE, since there's not a standard
# way to view them in GCE. Use the presence of minorversion as an
# indicator that it's GAE.
if self.minorversion:
result['logQuery'] = LogQueryV2String(result)
result['logViewUrl'] = LogViewUrl(result)
else:
result['consoleViewUrl'] = DebugViewUrl(result)
return result
def _LocationFromString(self, location):
"""Converts a file:line location string into a SourceLocation.
Args:
location: A string of the form file:line.
Returns:
The corresponding SourceLocation message.
Raises:
InvalidLocationException: if the line is not of the form path:line
"""
components = location.split(':')
if len(components) != 2:
raise errors.InvalidLocationException(
'Location must be of the form "path:line"')
try:
return self._debug_messages.SourceLocation(path=components[0],
line=int(components[1]))
except ValueError:
raise errors.InvalidLocationException(
'Location must be of the form "path:line", where "line" must be an '
'integer.')
def _FilteredDictListWithInfo(self, result, restrict_to_type):
"""Filters a result list to contain only breakpoints of the given type.
Args:
result: A list of breakpoint messages, to be filtered.
restrict_to_type: An optional breakpoint type. If None, no filtering
will be done.
Returns:
The filtered result, converted to equivalent dicts with debug info fields
added.
"""
return [self.AddTargetInfo(r) for r in result
if not restrict_to_type
or r.action == self.BreakpointAction(restrict_to_type)
or (not r.action and restrict_to_type == self.SNAPSHOT_TYPE)]
class _BreakpointWaiter(object):
"""Waits for multiple breakpoints.
Attributes:
_result_lock: Lock for modifications to all fields
_done: Flag to indicate that the wait condition is satisfied and wait
should stop even if some threads are not finished.
_threads: The list of active threads
_results: The set of completed breakpoints.
_failures: All exceptions which caused any thread to stop waiting.
_wait_all: If true, wait for all breakpoints to complete, else wait for
any breakpoint to complete. Controls whether to set _done after any
breakpoint completes.
_timeout: Mazimum time (in ms) to wait for breakpoints to complete.
"""
def __init__(self, wait_all, timeout):
self._result_lock = threading.Lock()
self._done = False
self._threads = []
self._results = {}
self._failures = []
self._wait_all = wait_all
self._timeout = timeout
def _IsComplete(self, response):
if response.breakpoint.isFinalState:
return True
with self._result_lock:
return self._done
def _WaitForOne(self, debuggee, breakpoint_id):
try:
breakpoint = debuggee.WaitForBreakpoint(
breakpoint_id, timeout=self._timeout,
completion_test=self._IsComplete)
if not breakpoint:
# Breakpoint never completed (i.e. timeout)
with self._result_lock:
if not self._wait_all:
self._done = True
return
if breakpoint.isFinalState:
with self._result_lock:
self._results[breakpoint_id] = breakpoint
if not self._wait_all:
self._done = True
except errors.DebugError as e:
with self._result_lock:
self._failures.append(e)
self._done = True
def AddTarget(self, debuggee, breakpoint_id):
self._threads.append(
threading.Thread(target=self._WaitForOne,
args=(debuggee, breakpoint_id)))
def Wait(self):
for t in self._threads:
t.start()
for t in self._threads:
t.join()
if self._failures:
# Just raise the first exception we handled
raise self._failures[0]
return self._results
def _FormatLocation(location):
if not location:
return None
return '{0}:{1}'.format(location.path, location.line)
def _BreakpointMatchesIdOrRegexp(breakpoint, ids, patterns):
"""Check if a breakpoint matches any of the given IDs or regexps.
Args:
breakpoint: Any _debug_messages.Breakpoint message object.
ids: A set of strings to search for exact matches on breakpoint ID.
patterns: A list of regular expressions to match against the file:line
location of the breakpoint.
Returns:
True if the breakpoint matches any ID or pattern.
"""
if breakpoint.id in ids:
return True
if not breakpoint.location:
return False
location = _FormatLocation(breakpoint.location)
for p in patterns:
if p.match(location):
return True
return False
def _FilterStaleMinorVersions(debuggees):
"""Filter out any debugees referring to a stale minor version.
Args:
debuggees: A list of Debuggee objects.
Returns:
A filtered list containing only the debuggees denoting the most recent
minor version with the given name. If any debuggee with a given name does
not have a 'minorversion' label, the resulting list will contain all
debuggees with that name.
"""
# First group by name
byname = {}
for debuggee in debuggees:
if debuggee.name in byname:
byname[debuggee.name].append(debuggee)
else:
byname[debuggee.name] = [debuggee]
# Now look at each list for a given name, choosing only the latest
# version.
result = []
for name_list in byname.values():
latest = _FindLatestMinorVersion(name_list)
if latest:
result.append(latest)
else:
result.extend(name_list)
return result
def _FindLatestMinorVersion(debuggees):
"""Given a list of debuggees, find the one with the highest minor version.
Args:
debuggees: A list of Debuggee objects.
Returns:
If all debuggees have the same name, return the one with the highest
integer value in its 'minorversion' label. If any member of the list does
not have a minor version, or if elements of the list have different
names, returns None.
"""
if not debuggees:
return None
best = None
best_version = None
name = None
for d in debuggees:
if not name:
name = d.name
elif name != d.name:
return None
minor_version = d.labels.get('minorversion', 0)
if not minor_version:
return None
try:
minor_version = int(minor_version)
if not best_version or minor_version > best_version:
best_version = minor_version
best = d
except ValueError:
# Got a bogus minor version. We can't determine which is best.
return None
return best
class _MessageDict(dict):
"""An extensible wrapper around message data.
Fields can be added as dictionary items and retrieved as attributes.
"""
def __init__(self, message, hidden_fields=None):
super(_MessageDict, self).__init__()
self._orig_type = type(message).__name__
if hidden_fields:
self._hidden_fields = hidden_fields
else:
self._hidden_fields = {}
for field in message.all_fields():
value = getattr(message, field.name)
if not value:
self._hidden_fields[field.name] = value
else:
self[field.name] = value
def __getattr__(self, attr):
if attr in self:
return self[attr]
if attr in self._hidden_fields:
return self._hidden_fields[attr]
raise AttributeError('Type "{0}" does not have attribute "{1}"'.format(
self._orig_type, attr))
def HideExistingField(self, field_name):
if field_name in self._hidden_fields:
return
self._hidden_fields[field_name] = self.pop(field_name, None)