Skip to content
This repository has been archived by the owner on Jul 18, 2024. It is now read-only.

Commit

Permalink
feat: list_debuggees supports active debuggees (#90)
Browse files Browse the repository at this point in the history
In support of #76 the `list_debuggees` command now:
- Shows only active debuggees by default
- Supports a `--include-inactive` flag
- Sorts the output with the most recently active debuggees being shown
  first.
  • Loading branch information
jasonborg authored Dec 9, 2022
1 parent fbf1f4d commit 13cac6c
Show file tree
Hide file tree
Showing 11 changed files with 949 additions and 231 deletions.
8 changes: 6 additions & 2 deletions snapshot_dbg_cli/COMMAND_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,20 @@ snapshot-cdbg-cli list_debuggees


Usage: `__main__.py list_debuggees [-h] [--database-url DATABASE_URL] [--format
FORMAT] [--debug]`
FORMAT] [--debug] [--include-inactive]`

Used to display a list of the debug targets (debuggees) registered with the
Snapshot Debugger.
Snapshot Debugger. By default all active debuggees are returned. To also obtain
inactive debuggees specify the --include-inactive option. considered to be
active if it was last running in the past 5-6 hours. A debuggee is considered
to be active if it currently running or last ran in the past 5-6 hours.

### Optional arguments

| Argument | Description |
|-------------------------------|-------------|
| `-h`, `--help` | Show this help message and exit. |
| `--include-inactive` | Include inactive debuggees. |
| `--database-url DATABASE_URL` | Specify the database URL for the CLI to use. This should only be used as an override to make the CLI talk to a specific instance and isn't expected to be needed. It is only required if the `--database-id` argument was used with the init command. This value may be specified either via this command line argument or via the `SNAPSHOT_DEBUGGER_DATABASE_URL` environment variable. When both are specified, the value from the command line takes precedence. |
| `--format FORMAT` | Set the format for printing command output resources. The default is a command-specific human-friendly output format. The supported formats are: `default`, `json` (raw) and `pretty-json` (formatted `json`). |
| `--debug` | Enable CLI debug messages. |
Expand Down
44 changes: 5 additions & 39 deletions snapshot_dbg_cli/breakpoint_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
These utilities are useful in multiple snapshot and logpoint commands.
"""

import datetime
import re
import snapshot_dbg_cli.time_utils

from snapshot_dbg_cli.status_message import StatusMessage

Expand Down Expand Up @@ -66,46 +66,12 @@ def transform_location_to_file_line(location):
return f"{location['path']}:{location['line']}"


# Returns the id to use when creating a new breakpoint.
# To note, we use the format 'b_<unix epoc seconds>'
#
# To note, this ensures the ID cannot be interpreted as an integer. This is
# specifically done for the following reason:


def convert_unix_msec_to_rfc3339(unix_msec):
"""Converts a Unix timestamp represented in milliseconds since the epoch to an
RFC3339 string representation.
Args:
unix_msec: The Unix timestamp, represented in milliseconds since the epoch.
Returns:
An RFC3339 encoded timestamp string in format: "%Y-%m-%dT%H:%M:%S.%fZ".
"""
try:
seconds = unix_msec / 1000
msec = unix_msec % 1000
timestamp = seconds
dt = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
return dt.strftime(f'%Y-%m-%dT%H:%M:%S.{msec:03}000') + 'Z'
except (OverflowError, OSError, TypeError, ValueError):
# By using 0, we'll still get the expected formatted string, and the value
# will be '1970-01-01...', which visually will be recognizable as beginning
# of epoch and that the value was not known.
return convert_unix_msec_to_rfc3339(0)


def set_converted_timestamps(bp):
conversions = [['createTime', 'createTimeUnixMsec'],
['finalTime', 'finalTimeUnixMsec']]

for c in conversions:
if c[0] not in bp and c[1] in bp:
bp[c[0]] = convert_unix_msec_to_rfc3339(bp[c[1]])
field_mappings = [('createTime', 'createTimeUnixMsec'),
('finalTime', 'finalTimeUnixMsec')]

return bp
return snapshot_dbg_cli.time_utils.set_converted_timestamps(
bp, field_mappings)


# Returns None if there's an issue
Expand Down
105 changes: 105 additions & 0 deletions snapshot_dbg_cli/debuggee_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Copyright 2022 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.
"""This module provides a variety of utilities related to debuggees processing.
"""

import snapshot_dbg_cli.time_utils

MSEC_PER_HOUR = 60 * 60 * 1000
DEBUGGEE_ACTIVE_THRESHOLD_MSEC = 6 * MSEC_PER_HOUR
DEBUGGEE_STALE_THRESHOLD_MSEC = 7 * 24 * MSEC_PER_HOUR


def get_display_name(labels):
module = labels.get('module', 'default')
version = labels.get('version', '')

return f'{module} - {version}'


def set_converted_timestamps(debuggee):
field_mappings = [('registrationTime', 'registrationTimeUnixMsec'),
('lastUpdateTime', 'lastUpdateTimeUnixMsec')]

return snapshot_dbg_cli.time_utils.set_converted_timestamps(
debuggee, field_mappings)


def normalize_debuggee(debuggee, current_time_unix_msec):
"""Validates and normalizes a debuggee.
This method ensures all required and expected fields are set. If any required
field is not set, and cannot be filled in, None will be returned.
If a debuggee is returned, the following fields are guaranteed to
be populated:
id
displayName
description
activeDebuggeeEnabled - Indicates the agent this debuggee represents
supports the 'active debuggee' feature, and so the
isActive and isStale fields are valid and accurate
to rely on. Early versions of the agents did not
populate the lastUpdateTimeUnixMsec field.
isActive - Indicates it has been recently active ~6hours. By default
this will be false if activeDebuggeeEnabled is false.
isStale - Indicates it has not been active for a long period
(~7days). By default this will be false if
activeDebuggeeEnabled is false.
lastUpdateTime
lastUpdateTimeUnixMsec
registrationTime
registrationTimeUnixMsec
Returns:
The normalized debuggee on success, None on failure.
"""
if not isinstance(debuggee, dict):
return None

required_fields = ['id']
for f in required_fields:
if f not in debuggee:
return None

if 'description' not in debuggee:
debuggee['description'] = ''

debuggee['displayName'] = get_display_name(debuggee.get('labels', {}))

debuggee['activeDebuggeeEnabled'] = 'lastUpdateTimeUnixMsec' in debuggee

if 'registrationTimeUnixMsec' not in debuggee:
# Debuggees created by older agents won't have this field present,
# initialize it to 0 so it's set to something, but it's clear the
# time was not actually known.
debuggee['registrationTimeUnixMsec'] = 0

if 'lastUpdateTimeUnixMsec' not in debuggee:
# Debuggees created by older agents won't have this field present,
# initialize it to 0 so it's set to something, but it's clear the
# time was not actually known.
debuggee['lastUpdateTimeUnixMsec'] = 0

set_converted_timestamps(debuggee)

debuggee['isActive'] = (
current_time_unix_msec -
debuggee['lastUpdateTimeUnixMsec']) <= DEBUGGEE_ACTIVE_THRESHOLD_MSEC
debuggee['isStale'] = (
current_time_unix_msec -
debuggee['lastUpdateTimeUnixMsec']) > DEBUGGEE_STALE_THRESHOLD_MSEC

return debuggee
54 changes: 30 additions & 24 deletions snapshot_dbg_cli/list_debuggees_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,17 @@
(debuggees) registered with the Snapshot Debugger.
"""

import time

DESCRIPTION = """
Used to display a list of the debug targets (debuggees) registered with the
Snapshot Debugger.
Snapshot Debugger. By default all active debuggees are returned. To also obtain
inactive debuggees specify the --include-inactive option. A debuggee is
considered to be active if it currently running or last ran in the past 5-6
hours.
"""


def validate_debuggee(debuggee):
required_fields = ['id']

return all(k in debuggee for k in required_fields)


def get_debuggee_name(debuggee):
module = debuggee.get('labels', {}).get('module', 'default')
version = debuggee.get('labels', {}).get('version', '')

return f'{module} - {version}'
INCLUDE_INACTIVE_HELP = 'Include inactive debuggees.'


class ListDebuggeesCommand:
Expand All @@ -52,28 +46,40 @@ def register(self, args_subparsers, required_parsers, common_parsers):
parent_parsers += required_parsers
parser = args_subparsers.add_parser(
'list_debuggees', description=DESCRIPTION, parents=parent_parsers)
parser.add_argument(
'--include-inactive', help=INCLUDE_INACTIVE_HELP, action='store_true')
parser.set_defaults(func=self.cmd)

def cmd(self, args, cli_services):
user_output = cli_services.user_output

current_time_unix_msec = int(time.time() * 1000)
debugger_rtdb_service = cli_services.get_snapshot_debugger_rtdb_service()
debuggees = debugger_rtdb_service.get_debuggees() or {}

# The result will be a dictionary, convert it to an array, while also
# filtering out any invalid entries, such as if it's missing a debuggee ID,
# which is a required field.
debuggees = list(filter(validate_debuggee, debuggees.values()))
debuggees = debugger_rtdb_service.get_debuggees(current_time_unix_msec)

if not args.include_inactive:
# If there are any debuggees that support the 'active debuggee' feature,
# then we go ahead and apply isActive filter. Any debuggees that don't
# support it will be filtered out, but given there are debuggees from
# newer agents present odds then that debugees without this feature are
# inactive.
if any(d['activeDebuggeeEnabled'] for d in debuggees):
debuggees = list(filter(lambda d: d['isActive'], debuggees))

# We add the second sort parameter on displayName for older agents that
# don't support the 'active debuggee' feature. They will all have the same
# lastUpdateTimeUnixMsec of 0, so they will still get some useful sorting.
debuggees = sorted(
debuggees,
key=lambda d: (d['lastUpdateTimeUnixMsec'], d['displayName']),
reverse=True)

if args.format.is_a_json_value():
user_output.json_format(debuggees, pretty=args.format.is_pretty_json())
else:
headers = ['Name', 'ID', 'Description']

values = [[
get_debuggee_name(d),
d.get('id', ''),
d.get('description', '')
] for d in debuggees]
values = [[d['displayName'], d['id'], d['description']] for d in debuggees
]

user_output.tabular(headers, values)
20 changes: 18 additions & 2 deletions snapshot_dbg_cli/snapshot_debugger_rtdb_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""

from snapshot_dbg_cli.breakpoint_utils import normalize_breakpoint
from snapshot_dbg_cli.debuggee_utils import normalize_debuggee
from snapshot_dbg_cli.exceptions import SilentlyExitError

import time
Expand Down Expand Up @@ -50,8 +51,18 @@ def get_schema_version(self):
def set_schema_version(self, version):
return self.rest_service.set(self.schema.get_path_schema_version(), version)

def get_debuggees(self):
return self.rest_service.get(self.schema.get_path_debuggees())
def get_debuggees(self, current_time_unix_msec):
debuggees = self.rest_service.get(self.schema.get_path_debuggees()) or {}

# The result will be a dictionary, convert it to an array, while also
# filtering out any invalid entries. normalize_debuggee returns None for
# invalid debuggees.
debuggees = [
dbgee for dbgee_id, dbgee in debuggees.items()
if normalize_debuggee(dbgee, current_time_unix_msec)
]

return debuggees

def validate_debuggee_id(self, debuggee_id):
"""Validates the debuggee ID exists.
Expand All @@ -75,6 +86,11 @@ def validate_debuggee_id(self, debuggee_id):
DEBUGGEE_NOT_FOUND_ERROR_MESSAGE.format(debuggee_id=debuggee_id))
raise SilentlyExitError

# Returns the id to use when creating a new breakpoint.
# To note, we use the format 'b_<unix epoc seconds>' this ensures the ID
# cannot be interpreted as an integer. This is specifically done for the
# following reason:
#
# Per
# https://firebase.googleblog.com/2014/04/best-practices-arrays-in-firebase.html
# "If all of the keys are integers, and more than half of the keys are between
Expand Down
Loading

0 comments on commit 13cac6c

Please sign in to comment.