Skip to content

Commit

Permalink
Branch
Browse files Browse the repository at this point in the history
  • Loading branch information
aBozowski committed Oct 19, 2023
1 parent 55e6dc1 commit 6ebf7c0
Show file tree
Hide file tree
Showing 27 changed files with 691 additions and 135 deletions.
1 change: 1 addition & 0 deletions src/tools/interop/idt/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ __pycache__/
pycache/
venv/
.zip
BUILD
3 changes: 1 addition & 2 deletions src/tools/interop/idt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,5 +308,4 @@ This module must contain a single class which is a subclass of
Note the following runtime expectations of platforms:
- Start should be able to be called repeatedly without restarting streaming.
- Stop should not cause an error even if the stream is not running.
- Connect, Start, and Stop may be called multiple times. i.e. start may be called when already streaming.
7 changes: 7 additions & 0 deletions src/tools/interop/idt/capture/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ def __init__(self, artifact_dir: str) -> None:
"""
raise NotImplementedError

@abstractmethod
async def connect(self) -> None:
"""
Establish connections to log targets for this platform
"""
raise NotImplementedError

@abstractmethod
async def start_streaming(self) -> None:
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def process_line(self, line: str) -> None:
getattr(self, line_func)(line)

def do_analysis(self) -> None:
with open(self.platform.logcat_output_path, mode='r') as logcat_file:
with open(self.platform.streams["LogcatStreamer"].logcat_artifact, mode='r') as logcat_file:
for line in logcat_file:
self.process_line(line)
self._show_analysis()
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
import os
from typing import Dict

from capture import log_format
from capture.base import EcosystemCapture, UnsupportedCapturePlatformException
from capture.file_utils import create_standard_log_name
from capture.platform.android import Android

from .analysis import PlayServicesAnalysis
from .command_map import dumpsys, getprop
from .prober import PlayServicesProber


class PlayServices(EcosystemCapture):
Expand All @@ -33,7 +35,7 @@ class PlayServices(EcosystemCapture):
"""

def __init__(self, platform: Android, artifact_dir: str) -> None:

self.logger = log_format.get_logger(__file__)
self.artifact_dir = artifact_dir

if not isinstance(platform, Android):
Expand All @@ -55,7 +57,7 @@ def __init__(self, platform: Android, artifact_dir: str) -> None:

def _write_standard_info_file(self) -> None:
for k, v in self.standard_info_data.items():
print(f"{k}: {v}")
self.logger.info(f"{k}: {v}")
standard_info_data_json = json.dumps(self.standard_info_data, indent=2)
with open(self.standard_info_file_path, mode='w+') as standard_info_file:
standard_info_file.write(standard_info_data_json)
Expand Down Expand Up @@ -94,3 +96,4 @@ async def stop_capture(self) -> None:

async def analyze_capture(self) -> None:
self.analysis.do_analysis()
await PlayServicesProber(self.platform).probe_services()
52 changes: 52 additions & 0 deletions src/tools/interop/idt/capture/ecosystem/play_services/prober.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#
# Copyright (c) 2023 Project CHIP Authors
# 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.
#

import os

from capture import log_format
from capture.shell_utils import Bash


class PlayServicesProber:

def __init__(self, platform):
self.platform = platform
self.artifact_dir = self.platform.artifact_dir
self.logger = log_format.get_logger(__file__)

async def _probe_foyer(self) -> None:
self.logger.info("probing remote services")
tgt = "googlehomefoyer-pa.googleapis.com"
probe_artifact = os.path.join(self.artifact_dir, "net_probes.txt")
command_suffix = f" 2>&1 | tee -a {probe_artifact}"
ping_cmd = f"ping -c 4 {tgt} {command_suffix}"
Bash(ping_cmd, sync=True).start_command()
trace_cmd_i = f"sudo traceroute {tgt} {command_suffix}"
Bash(trace_cmd_i, sync=True).start_command()
trace_cmd_t = f"sudo traceroute -T -p 443 {tgt} {command_suffix}"
Bash(trace_cmd_t, sync=True).start_command()
trace_cmd_u = f"sudo traceroute -U -p 443 {tgt} {command_suffix}"
Bash(trace_cmd_u, sync=True).start_command()
dig_cmd = f"dig {tgt} {command_suffix}"
Bash(dig_cmd, sync=True).start_command()
self.logger.info("probing from phone")
ping_from_phone = f"shell {ping_cmd} {command_suffix}"
self.platform.run_adb_command(ping_from_phone)

async def probe_services(self) -> None:
for probe_func in filter(lambda s: s.startswith('_probe'), dir(self)):
await getattr(self, probe_func)()
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async def stop_capture(self) -> None:
async def analyze_capture(self) -> None:
""""Show the start and end times of commissioning boundaries"""
analysis_file = open(self.analysis_file, mode='w+')
with open(self.platform.logcat_output_path, mode='r') as logcat_file:
with open(self.platform.streams["LogcatStreamer"].logcat_artifact, mode='r') as logcat_file:
for line in logcat_file:
if "CommissioningServiceBin: Binding to service" in line:
print_and_write(
Expand Down
51 changes: 38 additions & 13 deletions src/tools/interop/idt/capture/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@
import typing

import capture
from capture import log_format
from capture.base import EcosystemCapture, PlatformLogStreamer, UnsupportedCapturePlatformException
from capture.file_utils import border_print, safe_mkdir

_CONFIG_TIMEOUT = 45.0
_PLATFORM_MAP: typing.Dict[str, PlatformLogStreamer] = {}
_ECOSYSTEM_MAP: typing.Dict[str, PlatformLogStreamer] = {}
_ERROR_REPORT: typing.Dict[str, list[str]] = {}
logger = log_format.get_logger(__file__)


def _get_timeout():
Expand All @@ -41,16 +44,18 @@ def list_available_platforms() -> typing.List[str]:
return copy.deepcopy(capture.platform.__all__)

@staticmethod
def get_platform_impl(
async def get_platform_impl(
platform: str,
artifact_dir: str) -> PlatformLogStreamer:
if platform in _PLATFORM_MAP:
return _PLATFORM_MAP[platform]
border_print(f"Initializing platform {platform}")
platform_class = getattr(capture.platform, platform)
platform_artifact_dir = os.path.join(artifact_dir, platform)
safe_mkdir(platform_artifact_dir)
platform_inst = platform_class(platform_artifact_dir)
_PLATFORM_MAP[platform] = platform_inst
await platform_inst.connect()
return platform_inst


Expand All @@ -63,21 +68,23 @@ def list_available_ecosystems() -> typing.List[str]:
@staticmethod
async def get_ecosystem_impl(
ecosystem: str,
platform: str,
platform: PlatformLogStreamer,
artifact_dir: str) -> EcosystemCapture:
if ecosystem in _ECOSYSTEM_MAP:
return _ECOSYSTEM_MAP[ecosystem]
border_print(f"Initializing ecosystem {ecosystem}")
ecosystem_class = getattr(capture.ecosystem, ecosystem)
ecosystem_artifact_dir = os.path.join(artifact_dir, ecosystem)
safe_mkdir(ecosystem_artifact_dir)
platform_instance = PlatformFactory.get_platform_impl(
platform, artifact_dir)
ecosystem_instance = ecosystem_class(platform_instance, ecosystem_artifact_dir)
ecosystem_instance = ecosystem_class(platform, ecosystem_artifact_dir)
_ECOSYSTEM_MAP[ecosystem] = ecosystem_instance
return ecosystem_instance

@staticmethod
async def init_ecosystems(platform, ecosystem, artifact_dir):
async with asyncio.timeout_at(_get_timeout()):
platform = await PlatformFactory.get_platform_impl(
platform, artifact_dir)
ecosystems_to_load = EcosystemFactory.list_available_ecosystems() \
if ecosystem == 'ALL' \
else [ecosystem]
Expand All @@ -87,12 +94,18 @@ async def init_ecosystems(platform, ecosystem, artifact_dir):
await EcosystemFactory.get_ecosystem_impl(
ecosystem, platform, artifact_dir)
except UnsupportedCapturePlatformException:
print(f"ERROR unsupported platform {ecosystem} {platform}")
logger.error(f"Unsupported platform {ecosystem} {platform}")
except TimeoutError:
print(f"ERROR timeout starting ecosystem {ecosystem} {platform}")
logger.error(f"Timeout starting ecosystem {ecosystem} {platform}")
except Exception:
print("ERROR unknown error instantiating ecosystem")
print(traceback.format_exc())
logger.error("unknown error instantiating ecosystem")
logger.error(traceback.format_exc())


def track_error(ecosystem: str, error_type: str) -> None:
if ecosystem not in _ERROR_REPORT:
_ERROR_REPORT[ecosystem] = []
_ERROR_REPORT[ecosystem].append(error_type)


class EcosystemController:
Expand All @@ -102,14 +115,16 @@ async def handle_capture(attr):
attr = f"{attr}_capture"
for ecosystem in _ECOSYSTEM_MAP:
try:
border_print(f"{attr} capture for {ecosystem}")
border_print(f"{attr} for {ecosystem}")
async with asyncio.timeout_at(_get_timeout()):
await getattr(_ECOSYSTEM_MAP[ecosystem], attr)()
except TimeoutError:
print(f"ERROR timeout {attr} {ecosystem}")
logger.error(f"timeout {attr} {ecosystem}")
track_error(ecosystem, "TIMEOUT")
except Exception:
print(f"ERROR unexpected error {attr} {ecosystem}")
print(traceback.format_exc())
logger.error(f"unexpected error {attr} {ecosystem}")
logger.error(traceback.format_exc())
track_error(ecosystem, "UNEXPECTED")

@staticmethod
async def start():
Expand All @@ -121,4 +136,14 @@ async def stop():

@staticmethod
async def analyze():
# TODO: allow real time, not just post
await EcosystemController.handle_capture("analyze")

@staticmethod
def error_report():
for k, v in _ERROR_REPORT.items():
print(f"{k}: {v}")

@staticmethod
def has_errors():
return len(_ERROR_REPORT) > 0
11 changes: 6 additions & 5 deletions src/tools/interop/idt/capture/file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# limitations under the License.
#

import os
import time
from pathlib import Path
from typing import TextIO
Expand All @@ -30,23 +31,23 @@ def create_file_timestamp() -> str:
return time.strftime("%Y%m%d_%H%M%S")


def create_standard_log_name(name: str, ext: str) -> str:
def create_standard_log_name(name: str, ext: str, parent: str = "") -> str:
"""Returns the name argument wrapped as a standard log name"""
ts = create_file_timestamp()
return f'idt_{ts}_{name}.{ext}'
return os.path.join(parent, f'idt_{ts}_{name}.{ext}')


def safe_mkdir(dir_name: str) -> None:
Path(dir_name).mkdir(parents=True, exist_ok=True)


def print_and_write(to_print: str, file: TextIO) -> None:
print(to_print)
print(f"\x1b[32;1m{to_print}\x1b[0m")
file.write(to_print)


def border_print(to_print: str, important: bool = False) -> None:
len_borders = 64
len_borders = len(to_print)
border = f"\n{'_' * len_borders}\n"
i_border = f"\n{'!' * len_borders}\n" if important else ""
print(f"{border}{i_border}{to_print}{i_border}{border}")
print(f"\x1b[35;1m{border}{i_border}{to_print}{i_border}{border}\x1b[0m")
18 changes: 15 additions & 3 deletions src/tools/interop/idt/capture/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@
import importlib
import inspect
import os
import traceback
from typing import Any

from capture import log_format

logger = log_format.get_logger(__file__)


class CaptureImplsLoader:

def __init__(self, root_dir: str, root_package: str, search_type: type):
self.logger = logger
self.root_dir = root_dir
self.root_package = root_package
self.search_type = search_type
Expand All @@ -49,14 +55,17 @@ def verify_coroutines(self, subclass) -> bool:

def is_type_match(self, potential_class_match: Any) -> bool:
if inspect.isclass(potential_class_match):
self.logger.debug(f"Checking {self.search_type} match against {potential_class_match}")
if issubclass(potential_class_match, self.search_type):
self.logger.debug(f"Found type match search: {self.search_type} match: {potential_class_match}")
if self.verify_coroutines(potential_class_match):
return True
else:
print(f"WARNING missing coroutine {potential_class_match}")
self.logger.warning(f"Missing coroutine {potential_class_match}")
return False

def load_module(self, to_load):
self.logger.debug(f"Loading module {to_load}")
saw_more_than_one_impl = False
saw_one_impl = False
found_class = None
Expand All @@ -73,14 +82,17 @@ def load_module(self, to_load):
self.impl_names.append(found_class)
self.impls[found_class] = found_impl
elif saw_more_than_one_impl:
print(f"WARNING more than one impl in {module_item}")
self.logger.warning(f"more than one impl in {module_item}")

def fetch_impls(self):
self.logger.debug(f"Searching for implementations in {self.root_dir}")
for item in os.listdir(self.root_dir):
dir_content = os.path.join(self.root_dir, item)
if self.is_package(dir_content):
self.logger.debug(f"Found package in {dir_content}")
try:
module = importlib.import_module("." + item, self.root_package)
self.load_module(module)
except ModuleNotFoundError:
print(f"WARNING no module matching package name for {item}")
self.logger.warning(f"No module matching package name for {item}")
traceback.print_exc()
Loading

0 comments on commit 6ebf7c0

Please sign in to comment.