diff --git a/.github/.wordlist.txt b/.github/.wordlist.txt index 67a6d1d8fac3e4..4486fdafe19d48 100644 --- a/.github/.wordlist.txt +++ b/.github/.wordlist.txt @@ -298,8 +298,8 @@ connstring conntype const ContentApp -ContentApp's ContentAppPlatform +ContentApp's ContentLaunch ContentLauncher continuousHinting @@ -659,6 +659,8 @@ ICMP IDF IDL IDLs +idt +IDT idx ifconfig ifdef @@ -926,6 +928,7 @@ nl NLUnitTest NLUnitTests nmcli +nmtui noc NodeId nongnu @@ -1021,11 +1024,14 @@ params PartNumber PASE Passcode +passRetained +passwd PBKDF pbuf pbufs pbxproj PCA +pcap pcaps PDFs PDK @@ -1051,6 +1057,7 @@ PlatformManager PlatformManagerImpl plt png +Podman PollControl pollInterval polymorphism @@ -1147,8 +1154,8 @@ REPL repo req Requestor -Requestor's RequestorCanConsent +Requestor's Requestors responder RestrictedEvent @@ -1167,6 +1174,7 @@ rootfs RPC RPCs RPi +RPi's RPis RSA rsn @@ -1202,8 +1210,8 @@ SDB SDC SDHC SDK -SDK's sdkconfig +SDK's SDKs SDKTARGETSYSROOT sdl diff --git a/docs/tools/index.md b/docs/tools/index.md index 003573ed5ebb14..14ab8640c61bae 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -28,6 +28,7 @@ Source files for these tools are located at `scripts/tools`. ../scripts/tools/memory/README ../scripts/tools/spake2p/README +../src/tools/interop/idt/README ``` diff --git a/src/tools/interop/idt/.gitignore b/src/tools/interop/idt/.gitignore new file mode 100644 index 00000000000000..d2216005c33dc3 --- /dev/null +++ b/src/tools/interop/idt/.gitignore @@ -0,0 +1,9 @@ +*~ +.DS_Store +.idea/ +IDT_ARTIFACTS/ +OUT/ +__pycache__/ +pycache/ +venv/ +.zip diff --git a/src/tools/interop/idt/Dockerfile b/src/tools/interop/idt/Dockerfile new file mode 100644 index 00000000000000..6eeea77ac808fb --- /dev/null +++ b/src/tools/interop/idt/Dockerfile @@ -0,0 +1,48 @@ +# +# 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. +# + +FROM debian:bookworm +RUN apt-get update && \ + apt-get install -y \ + adb \ + aircrack-ng \ + apt-utils \ + git \ + glib-2.0 \ + kmod \ + libbluetooth-dev \ + libboost-python-dev \ + libboost-thread-dev \ + libglib2.0-dev \ + net-tools \ + pciutils \ + pkg-config \ + python3-pip \ + python3.11 \ + python3.11-venv \ + tcpdump \ + usbutils \ + wget && \ + echo "wireshark-common wireshark-common/install-setuid boolean true" | debconf-set-selections && \ + DEBIAN_FRONTEND=noninteractive apt-get -y install wireshark && \ + rm -rf /var/lib/apt/lists/* +RUN usermod -aG plugdev $(whoami) +RUN git clone https://github.com/openthread/ot-br-posix +COPY requirements.txt / +RUN /bin/bash -c "python3 -m venv /env && source /env/bin/activate && pip install -r requirements.txt" +RUN echo "source /env/bin/activate; pip -V; source /idt/scripts/alias.sh;" >> /root/.bashrc +ENTRYPOINT ["/bin/bash"] diff --git a/src/tools/interop/idt/README.md b/src/tools/interop/idt/README.md new file mode 100644 index 00000000000000..dc60067fba6fe3 --- /dev/null +++ b/src/tools/interop/idt/README.md @@ -0,0 +1,312 @@ +# Interoperability Debugging Tool + +## Overview + +The “Interoperability Debugging Tool” (IDT) is a python-based tool that supports +a variety of commands that are useful in the context of interoperability testing +of Matter devices and app controllers. + +### Discovery + +While in discovery mode, the tool displays all Matter devices that are in +commission and/or operational mode. This is useful to have a clear understanding +of all Matter devices currently “active” in the testing environment. + +See section “4.3. Discovery” of the Matter spec for official documentation. + +When run interactively, discovery functions in one of two modes: BLE and DNS-SD. + +### Capture + +While in capture mode, the tool starts capturing all data of interest (e.g. +video recording of interactions with the mobile app, logs from all components +involved, network packets capture, etc.) while a test is being conducted +manually. It also provides feedback to the user on test setup and execution. + +When the test completes, capture mode is stopped and all captured data is zipped +in a file that can then be sent to all parties involved in investigating any +issue uncovered via the manual test. Each ecosystem may implement an analysis +that analyzes capture data, displays info to the user, probes the local +environment and generates additional artifacts. + +## Getting started + +## Raspberry Pi installation + +### Environment overview + +The execution environment of IDT when using Raspberry Pi is shown in the figure +below. + +[TODO] add figure. + +The Raspberry Pi is where "discovery" and "capture" are executed. + +The "admin" computer is the machine used to connect to and control the RPi, and +to fetch artifacts which were created during capture from the RPi. + +This directory contains tools for use on both the admin computer and the RPi. + +### Environment details + +1. `idt` will be used on both the admin computer and the RPi. +1. `scripts` only points to one installation location at a time. It is ideal to + maintain a single `idt` directory on each (admin and RPi) system accordingly. +1. The expected install location on the RPi is the home directory of the user + specified in `idt/scripts/vars.sh`, which will be generated by running a + script in the next section. +1. Helper scripts may be used on admin computers that support `zsh` and `bash` + (Linux and macOS). +1. Windows may be used as the admin computer via tools like `PowerShell`, + `MobaXterm` and `FileZilla`. +1. This setup is intended to work with the admin computer and RPi connected to + the same Wi-Fi network, which is also the Wi-Fi network used for testing. +1. Corporate networks are not expected to be used as test networks. + +### Prepare the RPi + +1. A >= 128 GB SD card is recommended. +1. Flash the RPi SD with the debian based distribution of your choice. +1. Plug the SD into the RPi. +1. Ensure the RPi is connected to your network, either via ethernet or with + Wi-Fi configured in the disk image. +1. Boot the RPi. + +### Configure admin computer and push to the RPi + +#### Linux and macOS admin computers + +1. On your admin computer, source the `alias` script from the parent directory + of `idt` to get `idt` commands in your current shell. + ``` + source idt/scripts/alias.sh + ``` + - To avoid having to repeat this step for each session, optionally configure + automatic aliases permanently. + - **_NOTE:_** Once run, `idt` commands will be globally and automatically + available. If you need to remove the installation, edit the `.rc` files + mentioned in `setup_shell`. + ``` + source idt/scripts/setup_shell.sh + ``` +1. Run `idt_create_vars` and follow the prompts to set IDs for the target RPi. +1. Send `idt` to the RPi: + ``` + idt_push + ``` +1. `ssh` to the RPi: + - **_NOTE:_** You may need to wait a few minutes after boot for the `ssh` + server to be available on the RPi. Retry if needed! + ``` + idt_connect + ``` + +#### Windows admin computers + +1. Open `PowerShell`, cd to the directory containing `idt` and send `idt` to the + RPi: + ``` + scp -r ./idt/* $PIUSER@$PIHOST:/home/$PIUSER/idt + ``` +1. `ssh` to the RPi, e.g. with `MobaXterm` + - **_NOTE:_** You may need to wait a few minutes after boot for the `ssh` + server to be available on the RPi. Retry if needed! + - Use `$PIUSER@$PIHOST` or `$PIUSER@$ip` where `$ip` is the RPi's IP found + in your router admin panel. + +### Configure the RPi + +1. Configure passwords or ssh keys. +1. Configure Wi-Fi networks if needed. +1. Set up `idt`: + ``` + cd ~ # Go to idt parent dir + source idt/scripts/setup_shell.sh # Setup atuo aliases + source idt/scripts/alias.sh # Get aliases now + idt_bootstrap # Initial configuration + idt_build # Build the container image + ``` + +### Install updates + +SCP may not overwrite all files. To clear the `idt` dir off of the RPi safely +between pushes, exit the container and: + +``` +idt_clean +``` + +NOTE the idt artifacts directory is contained in idt, so running this will +delete any artifacts ([TODO] change). + +Then from the admin computer: + +``` +idt_push +``` + +## Single host installation (no Raspberry Pi) + +Follow the steps below to execute capture and discovery without a Raspberry Pi. + +### Linux installation + +#### Requirements + +- This package should work on most Debian (/based) systems. +- `idt` is currently tested on `Python 3.11`. +- `adb` and `tcpdump` are required. +- The machine running `idt` should be connected to the same Wi-Fi network used + for testing. + +#### Setup + +- From the parent directory of `idt`, run `source idt/scripts/alias.sh`. +- Optionally, run `source idt/scripts/setup_shell.sh` to install aliases + permanently. + +> You may use `idt` in a Python virtual environment OR using a container from +> the idt image. + +#### Python virtual environment + +- After `idt` aliases are available in your environment, calling any `idt` + command will automatically create a new virtual environment and install + dependencies. + +#### Docker + +- Run `idt_build` and `idt_activate` to enter the `idt` container. + +[TODO] Podman + +### macOS installation + +Most features other than BLE should work on macOS. + +Follow the Linux installation steps above, but do not use Docker. + +[TODO] macOS BLE support + +## User guide + +> **_IMPORTANT_** +> `idt_` commands are shell aliases helpful for administrative commands. +> `idt` invokes the `idt` python package. + +RPi users, as needed: + +- For users with Windows admin computers, reconnect e.g., using `MobaXterm` +- Other users reconnect `ssh` to the RPi (from your admin computer): + ``` + idt_connect + ``` +- Run the `idt` container (from the RPi): + ``` + idt_activate + ``` + +### Capture + +``` +idt capture -h + +usage: idt capture [-h] [--platform {Android}] + [--ecosystem {PlayServices,PlayServicesUser,ALL}] + [--pcap {t,f}] + [--interface {wlp0s20f3,docker0,lo}] + [--additional {t,f}] + +options: + -h, --help show this help message and exit + --platform {Android}, -p {Android} + Run capture for a particular platform + (default Android) + --ecosystem {PlayServices,PlayServicesUser,ALL}, -e {PlayServices,PlayServicesUser,ALL} + Run capture for a particular ecosystem or ALL + ecosystems (default ALL) + --pcap {t,f}, -c {t,f} + Run packet capture (default t) + --interface {wlp0s20f3,docker0,lo}, -i {wlp0s20f3,docker0,lo} + Run packet capture against a specified + interface (default wlp0s20f3) + --additional {t,f}, -a {t,f} + Run ble and mdns scanners in the background + while capturing (default t) +``` + +#### Artifacts + +Each ecosystem and platform involved in the capture will have their own +subdirectory in the root artifact dir. + +To download your artifacts, run these commands from your admin computer: + +`idt_fetch_artifacts` + +On windows admin computers, you may use `FileZilla` to pull the archive listed +at the end of output. + +### Discovery + +``` +idt discover -h + +usage: idt discover [-h] --type {ble,b,dnssd,d} + +options: + -h, --help show this help message and exit + --type {ble,b,dnssd,d}, -t {ble,b,dnssd,d} + Specify the type of discovery to execute +``` + +#### ble + +``` +idt discover -t b +``` + +#### mDNS + +``` +idt discover -t d +``` + +#### Artifacts + +There is a per device log for ble scanning in `ble` subdirectory of the root +artifact dir. + +[TODO] dnssd per device log + +## Extending functionality + +Ecosystem and Platform implementations are dynamically loaded. + +For each package in `capture/ecosystem`, the ecosystem loader expects a module +name matching the package name. +This module must contain a single class which is a subclass of +`capture.base.EcosystemCapture`. + +`/capture/ecosystem/play_services_user` contains a minimal example +implementation. + +As another example, link `/res/plugin_demo/ecosystem/demo_ext_ecosystem`. + +``` +$ idt_go && ln -s $PWD/idt/res/plugin_demo/ecosystem/demo_ext_ecosystem/ idt/capture/ecosystem +$ idt capture -h +usage: idt capture [-h] [--platform {Android}] [--ecosystem {DemoExtEcosystem... +``` + +The platform loader functions the same as `capture/ecosystem`. + +For each package in `capture/platform`, the platform loader expects a module +name matching the package name. +This module must contain a single class which is a subclass of +`capture.base.PlatformLogStreamer`. + +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. diff --git a/src/tools/interop/idt/__main__.py b/src/tools/interop/idt/__main__.py new file mode 100644 index 00000000000000..c354801687c553 --- /dev/null +++ b/src/tools/interop/idt/__main__.py @@ -0,0 +1,21 @@ +# +# 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. +# + +from idt import InteropDebuggingTool + +if __name__ == "__main__": + InteropDebuggingTool() diff --git a/src/tools/interop/idt/capture/__init__.py b/src/tools/interop/idt/capture/__init__.py new file mode 100644 index 00000000000000..d4e9baedc1119d --- /dev/null +++ b/src/tools/interop/idt/capture/__init__.py @@ -0,0 +1,32 @@ +# +# 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. +# + +from capture import ecosystem, platform + +from .factory import EcosystemCapture, EcosystemController, EcosystemFactory, PlatformFactory, PlatformLogStreamer +from .pcap import PacketCaptureRunner + +__all__ = [ + 'ecosystem', + 'platform', + 'EcosystemCapture', + 'EcosystemController', + 'EcosystemFactory', + 'PacketCaptureRunner', + 'PlatformFactory', + 'PlatformLogStreamer', +] diff --git a/src/tools/interop/idt/capture/base.py b/src/tools/interop/idt/capture/base.py new file mode 100644 index 00000000000000..6c1ab6fc8be8be --- /dev/null +++ b/src/tools/interop/idt/capture/base.py @@ -0,0 +1,90 @@ +# +# 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. +# + +from abc import ABC, abstractmethod + + +class PlatformLogStreamer(ABC): + """ + The abstract base class for a platform transport, subclassed by sub packages of platform + """ + + @abstractmethod + def __init__(self, artifact_dir: str) -> None: + """ + artifact_dir: the fully qualified path of the output directory. This directory already exists + """ + raise NotImplementedError + + @abstractmethod + async def start_streaming(self) -> None: + """ + Begin streaming logs + Start should be able to be called repeatedly without restarting streaming + """ + raise NotImplementedError + + @abstractmethod + async def stop_streaming(self) -> None: + """ + Stop streaming logs + Stop should not cause an error even if the stream is not running + """ + raise NotImplementedError + + +class UnsupportedCapturePlatformException(Exception): + """EcosystemCapture should raise this for unsupported platform""" + + def __init__(self, message: str): + super().__init__(message) + + +class EcosystemCapture(ABC): + + @abstractmethod + def __init__( + self, + platform: PlatformLogStreamer, + artifact_dir: str) -> None: + """ + platform: the instance of the log streamer for the selected platform + artifact_dir: the fully qualified path of the output directory. This directory already exists. + """ + raise NotImplementedError + + @abstractmethod + async def start_capture(self) -> None: + """ + Start the capture + """ + raise NotImplementedError + + @abstractmethod + async def stop_capture(self) -> None: + """ + Stop the capture + """ + raise NotImplementedError + + @abstractmethod + async def analyze_capture(self) -> None: + """ + Parse the capture and create + display helpful analysis artifacts that are unique to the ecosystem + Write analysis artifacts to artifact_dir + """ + raise NotImplementedError diff --git a/src/tools/interop/idt/capture/ecosystem/__init__.py b/src/tools/interop/idt/capture/ecosystem/__init__.py new file mode 100644 index 00000000000000..432af96d345a6b --- /dev/null +++ b/src/tools/interop/idt/capture/ecosystem/__init__.py @@ -0,0 +1,30 @@ +# +# 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. +# + +from capture.base import EcosystemCapture +from capture.loader import CaptureImplsLoader + +impl_loader = CaptureImplsLoader( + __path__[0], + "capture.ecosystem", + EcosystemCapture +) + +for impl_name, impl in impl_loader.impls.items(): + globals()[impl_name] = impl + +__all__ = impl_loader.impl_names diff --git a/src/tools/interop/idt/capture/ecosystem/play_services/__init__.py b/src/tools/interop/idt/capture/ecosystem/play_services/__init__.py new file mode 100644 index 00000000000000..ea3a4669e12cfd --- /dev/null +++ b/src/tools/interop/idt/capture/ecosystem/play_services/__init__.py @@ -0,0 +1,22 @@ +# +# 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. +# + +from .play_services import PlayServices + +__all__ = [ + 'PlayServices' +] diff --git a/src/tools/interop/idt/capture/ecosystem/play_services/analysis.py b/src/tools/interop/idt/capture/ecosystem/play_services/analysis.py new file mode 100644 index 00000000000000..46fcfc1c4d6734 --- /dev/null +++ b/src/tools/interop/idt/capture/ecosystem/play_services/analysis.py @@ -0,0 +1,95 @@ +# +# 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.file_utils import add_border, create_standard_log_name, print_and_write +from capture.platform.android import Android + + +class PlayServicesAnalysis: + + def __init__(self, platform: Android, artifact_dir: str) -> None: + self.artifact_dir = artifact_dir + self.analysis_file_name = os.path.join( + self.artifact_dir, create_standard_log_name( + 'commissioning_logcat', 'txt')) + + self.platform = platform + + self.matter_commissioner_logs = '' + self.failure_stack_trace = '' + self.pake_logs = '' + self.resolver_logs = '' + self.sigma_logs = '' + self.fail_trace_line_counter = -1 + + def _log_proc_matter_commissioner(self, line: str) -> None: + """Core commissioning flow""" + if 'MatterCommissioner' in line: + self.matter_commissioner_logs += line + + def _log_proc_commissioning_failed(self, line: str) -> None: + parsed_stack_trace_max_depth = 15 + if self.fail_trace_line_counter > parsed_stack_trace_max_depth: + self.fail_trace_line_counter = -1 + if self.fail_trace_line_counter > -1 and 'SetupDevice' in line: + self.failure_stack_trace += line + self.fail_trace_line_counter += 1 + if 'SetupDeviceView' and 'Commissioning failed' in line: + self.fail_trace_line_counter = 0 + self.failure_stack_trace += line + + def _log_proc_pake(self, line: str) -> None: + """Three logs for pake 1-3 expected""" + if "Pake" in line and "chip_logging" in line: + self.pake_logs += line + + def _log_proc_mdns(self, line: str) -> None: + if "_matter" in line and "ServiceResolverAdapter" in line: + self.resolver_logs += line + + def _log_proc_sigma(self, line: str) -> None: + """Three logs expected for sigma 1-3""" + if "Sigma" in line and "chip_logging" in line: + self.sigma_logs += line + + def _show_analysis(self) -> None: + analysis_file = open(self.analysis_file_name, mode="w+") + print_and_write(add_border('Matter commissioner logs'), analysis_file) + print_and_write(self.matter_commissioner_logs, analysis_file) + print_and_write( + add_border('Commissioning failure stack trace'), + analysis_file) + print_and_write(self.failure_stack_trace, analysis_file) + print_and_write(add_border('PASE Handshake'), analysis_file) + print_and_write(self.pake_logs, analysis_file) + print_and_write(add_border('DNS-SD resolution'), analysis_file) + print_and_write(self.resolver_logs, analysis_file) + print_and_write(add_border('CASE handshake'), analysis_file) + print_and_write(self.sigma_logs, analysis_file) + analysis_file.close() + + def process_line(self, line: str) -> None: + for line_func in filter(lambda s: s.startswith('_log'), dir(self)): + getattr(self, line_func)(line) + + def do_analysis(self) -> None: + with open(self.platform.logcat_output_path, mode='r') as logcat_file: + for line in logcat_file: + self.process_line(line) + self._show_analysis() diff --git a/src/tools/interop/idt/capture/ecosystem/play_services/command_map.py b/src/tools/interop/idt/capture/ecosystem/play_services/command_map.py new file mode 100644 index 00000000000000..4adf9b59bb5811 --- /dev/null +++ b/src/tools/interop/idt/capture/ecosystem/play_services/command_map.py @@ -0,0 +1,39 @@ +# +# 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. +# + +getprop = { + 'ro.product.model': 'android_model', + 'ro.build.version.release': 'android_version', + 'ro.build.version.sdk': 'android_api', + 'ro.build.fingerprint': 'build_fingerprint', + 'ro.odm.build.fingerprint': 'odm_build_fingerprint', + 'ro.product.build.fingerprint': 'product_build_fingerprint', + 'ro.ecosystem.build.fingerprint': 'vendor_build_fingerprint', +} + +_ap = 'activity provider com.google.android.gms.chimera.container.GmsModuleProvider' +dumpsys = { + 'display_width': 'display | grep StableDisplayWidth | awk -F\'=\' \'{print $2}\'', + 'display_height': 'display | grep StableDisplayHeight | awk -F\'=\' \'{print $2}\'', + 'gha_info': ' package com.google.android.apps.chromecast.app | grep versionName', + 'container_info': 'package com.google.android.gms | grep "versionName"', + 'home_module_info': f'{_ap} | grep "com.google.android.gms.home" | grep -v graph', + 'optional_home_module_info': f'{_ap} | grep "com.google.android.gms.optional_home" | grep -v graph', + 'policy_home_module_info': f'{_ap} | grep "com.google.android.gms.policy_home" | grep -v graph', + 'thread_info': f'{_ap} | grep "com.google.android.gms.threadnetwork"', + 'mdns_info': f'{_ap} | grep -i com.google.android.gms.mdns', +} diff --git a/src/tools/interop/idt/capture/ecosystem/play_services/play_services.py b/src/tools/interop/idt/capture/ecosystem/play_services/play_services.py new file mode 100644 index 00000000000000..aa9276844471ea --- /dev/null +++ b/src/tools/interop/idt/capture/ecosystem/play_services/play_services.py @@ -0,0 +1,96 @@ +# +# 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 json +import os +from typing import Dict + +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 + + +class PlayServices(EcosystemCapture): + """ + Implementation of capture and analysis for Play Services + """ + + def __init__(self, platform: Android, artifact_dir: str) -> None: + + self.artifact_dir = artifact_dir + + if not isinstance(platform, Android): + raise UnsupportedCapturePlatformException( + 'only platform=android is supported for ecosystem=play_services') + self.platform = platform + + self.standard_info_file_path = os.path.join( + self.artifact_dir, create_standard_log_name( + 'phone_info', 'json')) + self.standard_info_data: Dict[str, str] = {} + + self.analysis = PlayServicesAnalysis(self.platform, self.artifact_dir) + + self.service_ids = ['336', # Home + '305', # Thread + '168', # mDNS + ] + + def _write_standard_info_file(self) -> None: + for k, v in self.standard_info_data.items(): + print(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) + + def _parse_get_prop(self) -> None: + get_prop = self.platform.run_adb_command( + "shell getprop", + capture_output=True).get_captured_output() + for output in get_prop.split("\n"): + for prop in getprop: + if prop in output: + self.standard_info_data[prop] = output[output.rindex("["):] + + def _parse_dumpsys(self) -> None: + for attr_name, command in dumpsys.items(): + command = f"shell dumpsys {command}" + command_output = self.platform.run_adb_command( + command, + capture_output=True).get_captured_output() + self.standard_info_data[attr_name] = command_output + + def _get_standard_info(self) -> None: + self._parse_get_prop() + self._parse_dumpsys() + self._write_standard_info_file() + + async def start_capture(self) -> None: + for service_id in self.service_ids: + verbose_command = f"shell setprop log.tag.gms_svc_id:{service_id} VERBOSE" + self.platform.run_adb_command(verbose_command) + self._get_standard_info() + await self.platform.start_streaming() + + async def stop_capture(self) -> None: + await self.platform.stop_streaming() + + async def analyze_capture(self) -> None: + self.analysis.do_analysis() diff --git a/src/tools/interop/idt/capture/ecosystem/play_services_user/__init__.py b/src/tools/interop/idt/capture/ecosystem/play_services_user/__init__.py new file mode 100644 index 00000000000000..ea02f9df6b375f --- /dev/null +++ b/src/tools/interop/idt/capture/ecosystem/play_services_user/__init__.py @@ -0,0 +1,22 @@ +# +# 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. +# + +from .play_services_user import PlayServicesUser + +__all__ = [ + 'PlayServicesUser' +] diff --git a/src/tools/interop/idt/capture/ecosystem/play_services_user/play_services_user.py b/src/tools/interop/idt/capture/ecosystem/play_services_user/play_services_user.py new file mode 100644 index 00000000000000..5044842e80eb28 --- /dev/null +++ b/src/tools/interop/idt/capture/ecosystem/play_services_user/play_services_user.py @@ -0,0 +1,66 @@ +# +# 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.base import EcosystemCapture, UnsupportedCapturePlatformException +from capture.file_utils import create_standard_log_name, print_and_write +from capture.platform.android.android import Android + + +class PlayServicesUser(EcosystemCapture): + """ + Implementation of capture and analysis for Play Services 3P + """ + + def __init__(self, platform: Android, artifact_dir: str) -> None: + + self.artifact_dir = artifact_dir + self.analysis_file = os.path.join( + self.artifact_dir, create_standard_log_name( + 'commissioning_boundaries', 'txt')) + + if not isinstance(platform, Android): + raise UnsupportedCapturePlatformException( + 'only platform=android is supported for ' + 'ecosystem=PlayServicesUser') + self.platform = platform + + async def start_capture(self) -> None: + await self.platform.start_streaming() + + async def stop_capture(self) -> None: + await self.platform.stop_streaming() + + 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: + for line in logcat_file: + if "CommissioningServiceBin: Binding to service" in line: + print_and_write( + f"3P commissioner initiated Play Services commissioning\n{line}", + analysis_file) + elif "CommissioningServiceBin: Sending commissioning request to bound service" in line: + print_and_write( + f"Play Services commissioning complete; passing back to 3P\n{line}", + analysis_file) + elif "CommissioningServiceBin: Received commissioning complete from bound service" in line: + print_and_write( + f"3P commissioning complete!\n{line}", + analysis_file) + analysis_file.close() diff --git a/src/tools/interop/idt/capture/factory.py b/src/tools/interop/idt/capture/factory.py new file mode 100644 index 00000000000000..3c86045f146a0e --- /dev/null +++ b/src/tools/interop/idt/capture/factory.py @@ -0,0 +1,124 @@ +# +# 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 asyncio +import copy +import os +import traceback +import typing + +import capture +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] = {} + + +def _get_timeout(): + return asyncio.get_running_loop().time() + _CONFIG_TIMEOUT + + +class PlatformFactory: + + @staticmethod + def list_available_platforms() -> typing.List[str]: + return copy.deepcopy(capture.platform.__all__) + + @staticmethod + def get_platform_impl( + platform: str, + artifact_dir: str) -> PlatformLogStreamer: + if platform in _PLATFORM_MAP: + return _PLATFORM_MAP[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 + return platform_inst + + +class EcosystemFactory: + + @staticmethod + def list_available_ecosystems() -> typing.List[str]: + return copy.deepcopy(capture.ecosystem.__all__) + + @staticmethod + async def get_ecosystem_impl( + ecosystem: str, + platform: str, + artifact_dir: str) -> EcosystemCapture: + if ecosystem in _ECOSYSTEM_MAP: + return _ECOSYSTEM_MAP[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_MAP[ecosystem] = ecosystem_instance + return ecosystem_instance + + @staticmethod + async def init_ecosystems(platform, ecosystem, artifact_dir): + ecosystems_to_load = EcosystemFactory.list_available_ecosystems() \ + if ecosystem == 'ALL' \ + else [ecosystem] + for ecosystem in ecosystems_to_load: + try: + async with asyncio.timeout_at(_get_timeout()): + await EcosystemFactory.get_ecosystem_impl( + ecosystem, platform, artifact_dir) + except UnsupportedCapturePlatformException: + print(f"ERROR unsupported platform {ecosystem} {platform}") + except TimeoutError: + print(f"ERROR timeout starting ecosystem {ecosystem} {platform}") + except Exception: + print("ERROR unknown error instantiating ecosystem") + print(traceback.format_exc()) + + +class EcosystemController: + + @staticmethod + async def handle_capture(attr): + attr = f"{attr}_capture" + for ecosystem in _ECOSYSTEM_MAP: + try: + border_print(f"{attr} capture for {ecosystem}") + async with asyncio.timeout_at(_get_timeout()): + await getattr(_ECOSYSTEM_MAP[ecosystem], attr)() + except TimeoutError: + print(f"ERROR timeout {attr} {ecosystem}") + except Exception: + print(f"ERROR unexpected error {attr} {ecosystem}") + print(traceback.format_exc()) + + @staticmethod + async def start(): + await EcosystemController.handle_capture("start") + + @staticmethod + async def stop(): + await EcosystemController.handle_capture("stop") + + @staticmethod + async def analyze(): + await EcosystemController.handle_capture("analyze") diff --git a/src/tools/interop/idt/capture/file_utils.py b/src/tools/interop/idt/capture/file_utils.py new file mode 100644 index 00000000000000..29aabfe6133193 --- /dev/null +++ b/src/tools/interop/idt/capture/file_utils.py @@ -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 time +from pathlib import Path +from typing import TextIO + + +def add_border(to_print: str) -> str: + """Add star borders to important strings""" + return '\n' + '*' * len(to_print) + '\n' + to_print + + +def create_file_timestamp() -> str: + """Conventional file timestamp suffix""" + return time.strftime("%Y%m%d_%H%M%S") + + +def create_standard_log_name(name: str, ext: str) -> str: + """Returns the name argument wrapped as a standard log name""" + ts = create_file_timestamp() + return 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) + file.write(to_print) + + +def border_print(to_print: str, important: bool = False) -> None: + len_borders = 64 + 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}") diff --git a/src/tools/interop/idt/capture/loader.py b/src/tools/interop/idt/capture/loader.py new file mode 100644 index 00000000000000..27fd7c19dd6560 --- /dev/null +++ b/src/tools/interop/idt/capture/loader.py @@ -0,0 +1,86 @@ +# +# 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 importlib +import inspect +import os +from typing import Any + + +class CaptureImplsLoader: + + def __init__(self, root_dir: str, root_package: str, search_type: type): + self.root_dir = root_dir + self.root_package = root_package + self.search_type = search_type + self.impl_names = [] + self.impls = {} + self.fetch_impls() + + @staticmethod + def is_package(potential_package: str) -> bool: + init_path = os.path.join(potential_package, + "__init__.py") + return os.path.exists(init_path) + + def verify_coroutines(self, subclass) -> bool: + for item in dir(self.search_type): + item_attr = getattr(self.search_type, item) + if inspect.iscoroutinefunction(item_attr): + if not hasattr(subclass, item): + return False + if not inspect.iscoroutinefunction(getattr(subclass, item)): + return False + return True + + def is_type_match(self, potential_class_match: Any) -> bool: + if inspect.isclass(potential_class_match): + if issubclass(potential_class_match, self.search_type): + if self.verify_coroutines(potential_class_match): + return True + else: + print(f"WARNING missing coroutine {potential_class_match}") + return False + + def load_module(self, to_load): + saw_more_than_one_impl = False + saw_one_impl = False + found_class = None + for module_item in dir(to_load): + loaded_item = getattr(to_load, module_item) + if self.is_type_match(loaded_item): + found_class = module_item + found_impl = loaded_item + if not saw_one_impl: + saw_one_impl = True + else: + saw_more_than_one_impl = True + if saw_one_impl and not saw_more_than_one_impl: + 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}") + + def fetch_impls(self): + for item in os.listdir(self.root_dir): + dir_content = os.path.join(self.root_dir, item) + if self.is_package(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}") diff --git a/src/tools/interop/idt/capture/pcap/__init__.py b/src/tools/interop/idt/capture/pcap/__init__.py new file mode 100644 index 00000000000000..b536056a1c0add --- /dev/null +++ b/src/tools/interop/idt/capture/pcap/__init__.py @@ -0,0 +1,22 @@ +# +# 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. +# + +from .pcap import PacketCaptureRunner + +__all__ = [ + 'PacketCaptureRunner' +] diff --git a/src/tools/interop/idt/capture/pcap/pcap.py b/src/tools/interop/idt/capture/pcap/pcap.py new file mode 100644 index 00000000000000..8208d59f87d780 --- /dev/null +++ b/src/tools/interop/idt/capture/pcap/pcap.py @@ -0,0 +1,61 @@ +# +# 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 +import time + +from capture.file_utils import create_standard_log_name +from capture.shell_utils import Bash + + +class PacketCaptureRunner: + + def __init__(self, artifact_dir: str, interface: str) -> None: + + self.artifact_dir = artifact_dir + self.output_path = str( + os.path.join( + self.artifact_dir, + create_standard_log_name( + "pcap", + "cap"))) + self.start_delay_seconds = 2 + self.interface = interface + self.pcap_command = f"tcpdump -i {self.interface} -n -w {self.output_path}" + self.pcap_proc = Bash(self.pcap_command) + + def start_pcap(self) -> None: + self.pcap_proc.start_command() + print("Pausing to check if pcap started...") + time.sleep(self.start_delay_seconds) + if not self.pcap_proc.command_is_running(): + print( + "Pcap did not start, you might need root; please authorize if prompted.") + Bash("sudo echo \"\"", sync=True) + print("Retrying pcap with sudo...") + self.pcap_command = f"sudo {self.pcap_command}" + self.pcap_proc = Bash(self.pcap_command) + self.pcap_proc.start_command() + time.sleep(self.start_delay_seconds) + if not self.pcap_proc.command_is_running(): + print("WARNING Failed to start pcap!") + else: + print(f"Pcap output path {self.output_path}") + + def stop_pcap(self) -> None: + self.pcap_proc.stop_command(soft=True) + print("Pcap stopped") diff --git a/src/tools/interop/idt/capture/platform/__init__.py b/src/tools/interop/idt/capture/platform/__init__.py new file mode 100644 index 00000000000000..92c1efcce79497 --- /dev/null +++ b/src/tools/interop/idt/capture/platform/__init__.py @@ -0,0 +1,30 @@ +# +# 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. +# + +from capture.base import PlatformLogStreamer +from capture.loader import CaptureImplsLoader + +impl_loader = CaptureImplsLoader( + __path__[0], + "capture.platform", + PlatformLogStreamer +) + +for impl_name, impl in impl_loader.impls.items(): + globals()[impl_name] = impl + +__all__ = impl_loader.impl_names diff --git a/src/tools/interop/idt/capture/platform/android/__init__.py b/src/tools/interop/idt/capture/platform/android/__init__.py new file mode 100644 index 00000000000000..7ec319feb0e9a3 --- /dev/null +++ b/src/tools/interop/idt/capture/platform/android/__init__.py @@ -0,0 +1,22 @@ +# +# 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. +# + +from .android import Android + +__all__ = [ + 'Android' +] diff --git a/src/tools/interop/idt/capture/platform/android/android.py b/src/tools/interop/idt/capture/platform/android/android.py new file mode 100644 index 00000000000000..2a394495835ef5 --- /dev/null +++ b/src/tools/interop/idt/capture/platform/android/android.py @@ -0,0 +1,217 @@ +# +# 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 asyncio +import ipaddress +import os +import typing + +from capture.base import PlatformLogStreamer +from capture.file_utils import create_standard_log_name +from capture.shell_utils import Bash + + +class Android(PlatformLogStreamer): + """ + Class that supports: + - Running synchronous adb commands + - Maintaining a singleton logcat stream + - Maintaining a singleton screen recording + """ + + def __init__(self, artifact_dir: str) -> None: + + self.artifact_dir = artifact_dir + + self.device_id: str | None = None + self.adb_devices: typing.Dict[str, bool] = {} + self._authorize_adb() + + self.logcat_output_path = os.path.join( + self.artifact_dir, create_standard_log_name( + 'logcat', 'txt')) + self.logcat_command = f'adb -s {self.device_id} logcat -T 1 >> {self.logcat_output_path}' + self.logcat_proc = Bash(self.logcat_command) + + screen_cast_name = create_standard_log_name('screencast', 'mp4') + self.screen_cap_output_path = os.path.join( + self.artifact_dir, screen_cast_name) + self.check_screen_command = "shell dumpsys deviceidle | grep mScreenOn" + self.screen_path = f'/sdcard/Movies/{screen_cast_name}' + self.screen_command = f'adb -s {self.device_id} shell screenrecord --bugreport {self.screen_path}' + self.screen_proc = Bash(self.screen_command) + self.pull_screen = False + self.screen_pull_command = f'pull {self.screen_path} {self.screen_cap_output_path}' + + def run_adb_command( + self, + command: str, + capture_output: bool = False) -> Bash: + """ + Run an adb command synchronously + Capture_output must be true to call get_captured_output() later + """ + return Bash( + f'adb -s {self.device_id} {command}', + sync=True, + capture_output=capture_output) + + def get_adb_devices(self) -> typing.Dict[str, bool]: + """Returns a dict of device ids and whether they are authorized""" + adb_devices = Bash('adb devices', sync=True, capture_output=True) + adb_devices_output = adb_devices.get_captured_output().split('\n') + devices_auth = {} + header_done = False + for line in adb_devices_output: + if header_done: + line_parsed = line.split("\t") + device_id = line_parsed[0] + device_is_auth = line_parsed[1] == "device" + if line_parsed[1] == "offline": + disconnect_command = f"adb disconnect {device_id}" + print(f"Device {device_id} is offline, trying disconnect!") + Bash( + disconnect_command, + sync=True, + capture_output=False) + else: + devices_auth[device_id] = device_is_auth + header_done = True + self.adb_devices = devices_auth + return self.adb_devices + + def _only_one_device_connected(self) -> bool: + return len(self.adb_devices) == 1 + + def _get_first_connected_device(self) -> str: + return list(self.adb_devices.keys())[0] + + def _set_device_if_only_one_connected(self) -> None: + if self._only_one_device_connected(): + self.device_id = self._get_first_connected_device() + print(f'Only one device detected; using {self.device_id}') + + def _log_adb_devices(self) -> None: + for dev in self.adb_devices: + print(dev) + + @staticmethod + def _is_connection_str(adb_input_str: str) -> bool: + valid_ipv4 = False + port_entered = False + valid_port = False + split_on_colon = adb_input_str.split(":") + try: + ipaddress.IPv4Network(split_on_colon[0]) + valid_ipv4 = True + except ValueError: + pass + if len(split_on_colon) > 1: + port_entered = True + try: + port = int(split_on_colon[1]) + valid_port = port < 65535 + except ValueError: + pass + valid_ip_no_port = valid_ipv4 and not port_entered + valid_ip_valid_port = valid_ipv4 and valid_port + return valid_ip_no_port or valid_ip_valid_port + + def _check_connect_wireless_adb(self, temp_device_id: str) -> None: + if Android._is_connection_str(temp_device_id): + connect_command = f"adb connect {temp_device_id}" + print( + f"Detected connection string; attempting to connect: {connect_command}") + Bash(connect_command, sync=True, capture_output=False) + self.get_adb_devices() + + def _device_id_user_input(self) -> None: + print('If there is no output below, press enter after connecting your phone under test OR') + print('Enter (copy paste) the target device id from the list of available devices below OR') + print('Enter $IP4:$PORT to connect wireless debugging.') + self._log_adb_devices() + temp_device_id = input('').strip() + self._check_connect_wireless_adb(temp_device_id) + if self._only_one_device_connected(): + self._set_device_if_only_one_connected() + elif temp_device_id not in self.adb_devices: + print('Entered device not in adb devices!') + else: + self.device_id = temp_device_id + + def _choose_device_id(self) -> None: + """ + Prompts the user to select a single device ID for this transport + If only one device is ever connected, use it. + """ + self._set_device_if_only_one_connected() + while self.device_id not in self.get_adb_devices(): + self._device_id_user_input() + print(f'Selected device {self.device_id}') + + def _authorize_adb(self) -> None: + """ + Prompts the user until a single device is selected and adb is auth'd + """ + self.get_adb_devices() + self._choose_device_id() + while not self.get_adb_devices()[self.device_id]: + print('Confirming authorization, press enter after auth') + input('') + print(f'Target android device ID is authorized: {self.device_id}') + + def check_screen(self) -> bool: + screen_cmd_output = self.run_adb_command( + self.check_screen_command, capture_output=True) + return "true" in screen_cmd_output.get_captured_output() + + async def prepare_screen_recording(self) -> None: + if self.screen_proc.command_is_running(): + return + try: + async with asyncio.timeout_at(asyncio.get_running_loop().time() + 20.0): + screen_on = self.check_screen() + print("Please turn the screen on so screen recording can start!") + while not screen_on: + await asyncio.sleep(2) + screen_on = self.check_screen() + if not screen_on: + print("Screen is still not on for recording!") + except TimeoutError: + print("WARNING screen recording timeout") + return + + async def start_streaming(self) -> None: + await self.prepare_screen_recording() + if self.check_screen(): + self.pull_screen = True + self.screen_proc.start_command() + self.logcat_proc.start_command() + + async def pull_screen_recording(self) -> None: + if self.pull_screen: + self.screen_proc.stop_command() + print("screen proc stopped") + await asyncio.sleep(3) + self.run_adb_command(self.screen_pull_command) + print("screen recording pull attempted") + self.pull_screen = False + + async def stop_streaming(self) -> None: + await self.pull_screen_recording() + self.logcat_proc.stop_command() + print("logcat stopped") diff --git a/src/tools/interop/idt/capture/shell_utils.py b/src/tools/interop/idt/capture/shell_utils.py new file mode 100644 index 00000000000000..92bb11cbc25a8f --- /dev/null +++ b/src/tools/interop/idt/capture/shell_utils.py @@ -0,0 +1,78 @@ +# +# 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 shlex +import subprocess + +from mobly.utils import stop_standing_subprocess + + +class Bash: + """ + Uses subprocess to execute bash commands + Intended to be instantiated and then only interacted with through instance methods + """ + + def __init__(self, command: str, sync: bool = False, + capture_output: bool = False) -> None: + """ + Run a bash command as a sub process + :param command: Command to run + :param sync: If True, wait for command to terminate + :param capture_output: Only applies to sync; if True, store stdout and stderr + """ + self.command: str = command + self.sync = sync + self.capture_output = capture_output + + self.args: list[str] = [] + self._init_args() + self.proc = subprocess.run(self.args, capture_output=capture_output) if self.sync else None + + def _init_args(self) -> None: + """Escape quotes, call bash, and prep command for subprocess args""" + command_escaped = self.command.replace('"', '\"') + self.args = shlex.split(f'/bin/bash -c "{command_escaped}"') + + def command_is_running(self) -> bool: + return self.proc is not None and self.proc.poll() is None + + def get_captured_output(self) -> str: + """Return captured output when the relevant instance var is set""" + return "" if not self.capture_output or not self.sync \ + else self.proc.stdout.decode().strip() + + def start_command(self) -> None: + if not self.sync and not self.command_is_running(): + self.proc = subprocess.Popen(self.args) + else: + print(f'INFO {self.command} start requested while running') + + def stop_command(self, soft: bool = False) -> None: + if self.command_is_running(): + if soft: + self.proc.terminate() + if self.proc.stdout: + self.proc.stdout.close() + if self.proc.stderr: + self.proc.stderr.close() + self.proc.wait() + else: + stop_standing_subprocess(self.proc) + else: + print(f'INFO {self.command} stop requested while not running') + self.proc = None diff --git a/src/tools/interop/idt/discovery/__init__.py b/src/tools/interop/idt/discovery/__init__.py new file mode 100644 index 00000000000000..f9a1aa665a86d8 --- /dev/null +++ b/src/tools/interop/idt/discovery/__init__.py @@ -0,0 +1,23 @@ +# +# 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. +# +from .matter_ble import MatterBleScanner +from .matter_dnssd import MatterDnssdListener + +__all__ = [ + 'MatterBleScanner', + 'MatterDnssdListener' +] diff --git a/src/tools/interop/idt/discovery/matter_ble.py b/src/tools/interop/idt/discovery/matter_ble.py new file mode 100644 index 00000000000000..737842bca70ab8 --- /dev/null +++ b/src/tools/interop/idt/discovery/matter_ble.py @@ -0,0 +1,104 @@ +# +# 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 asyncio +import datetime +import logging +import os +import time + +from bleak import AdvertisementData, BleakScanner, BLEDevice +from bleak.exc import BleakDBusError + + +class MatterBleScanner: + + def __init__(self, artifact_dir: str): + self.artifact_dir = artifact_dir + self.logger = logging.getLogger(__file__) + self.devices_seen_last_time: set[str] = set() + self.devices_seen_this_time: set[str] = set() + self.throttle_seconds = 1 + self.error_seconds = 5 + + def parse_vid_pid(self, loggable_data: str) -> str: + try: + vid = loggable_data[8:10] + loggable_data[6:8] + pid = loggable_data[12:14] + loggable_data[10:12] + except IndexError: + self.logger.warning("Error parsing vid / pid from BLE ad data") + return "" + return f"VID: {vid} PID: {pid}" + + def write_device_log(self, device_name: str, to_write: str) -> None: + log_file_name = os.path.join(self.artifact_dir, f"{device_name}.txt") + with open(log_file_name, "a+") as log_file: + ts = datetime.datetime.now().isoformat(sep=' ', timespec='milliseconds') + to_write = f"{ts}\n{to_write}\n\n" + log_file.write(to_write) + print(to_write) + + @staticmethod + def is_matter_device(service_uuid: str) -> bool: + is_matter_device = service_uuid.startswith("0000fff6") + return is_matter_device + + def handle_device_states(self) -> None: + for device_id in self.devices_seen_last_time - self.devices_seen_this_time: + to_log = f"LOST {device_id}" + self.write_device_log(device_id, to_log) + self.devices_seen_last_time = self.devices_seen_this_time + self.devices_seen_this_time = set() + + def log_ble_discovery( + self, + name: str, + bin_data: bytes, + ble_device: BLEDevice, + rssi: int) -> None: + loggable_data = bin_data.hex() + if self.is_matter_device(name): + device_id = f"{ble_device.name}_{ble_device.address}" + self.devices_seen_this_time.add(device_id) + if device_id not in self.devices_seen_last_time: + to_log = f"DISCOVERED\n{ble_device.name} {ble_device.address}" + to_log += f"{name}\n{loggable_data}\n" + to_log += f"RSSI {rssi}\n" + to_log += self.parse_vid_pid(loggable_data) + self.write_device_log(device_id, to_log) + + async def browse(self, scanner: BleakScanner) -> None: + devices: dict[str, tuple[BLEDevice, AdvertisementData]] = await scanner.discover(return_adv=True) + for device in devices.values(): + ble_device = device[0] + ad_data = device[1] + for name, bin_data in ad_data.service_data.items(): + self.log_ble_discovery( + name, bin_data, ble_device, ad_data.rssi) + self.handle_device_states() + + def browse_interactive(self) -> None: + scanner = BleakScanner() + self.logger.warning( + "Scanning BLE\nDCL Lookup: https://webui.dcl.csa-iot.org/") + while True: + try: + time.sleep(self.throttle_seconds) + asyncio.run(self.browse(scanner)) + except BleakDBusError as e: + self.logger.warning(e) + time.sleep(self.error_seconds) diff --git a/src/tools/interop/idt/discovery/matter_dnssd.py b/src/tools/interop/idt/discovery/matter_dnssd.py new file mode 100644 index 00000000000000..7dfec195174313 --- /dev/null +++ b/src/tools/interop/idt/discovery/matter_dnssd.py @@ -0,0 +1,89 @@ +# +# 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 logging + +from zeroconf import ServiceBrowser, ServiceInfo, ServiceListener, Zeroconf + +_MDNS_TYPES = { + "_matterd._udp.local.": "COMMISSIONER", + "_matterc._udp.local.": "COMMISSIONABLE", + "_matter._tcp.local.": "OPERATIONAL", + "_meshcop._udp.local.": "THREAD_BORDER_ROUTER", +} + + +class MatterDnssdListener(ServiceListener): + + def __init__(self, artifact_dir: str) -> None: + super().__init__() + self.artifact_dir = artifact_dir + self.logger = logging.getLogger(__file__) + + @staticmethod + def log_addr(info: ServiceInfo) -> str: + ret = "\n" + for addr in info.parsed_scoped_addresses(): + ret += f"{addr}\n" + return ret + + @staticmethod + def log_vid_pid(info: ServiceInfo) -> str: + if info.properties is not None and b'VP' in info.properties: + vid_pid = str(info.properties[b'VP']) + vid_pid = vid_pid[2:len(vid_pid) - 1].split('+') + vid = hex(int(vid_pid[0])) + pid = hex(int(vid_pid[1])) + return f"\nVID: {vid} PID: {pid}\n" + return "" + + def handle_service_info( + self, + zc: Zeroconf, + type_: str, + name: str, + delta_type: str) -> None: + info = zc.get_service_info(type_, name) + to_log = f"{name}\n" + if info.properties is not None: + for name, value in info.properties.items(): + to_log += f"{name}:\t{value}\n" + update_str = f"\nSERVICE {delta_type}\n" + to_log += ("*" * (len(update_str) - 2)) + update_str + to_log += _MDNS_TYPES[type_] + to_log += self.log_vid_pid(info) + to_log += self.log_addr(info) + self.logger.info(to_log) + + def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: + self.handle_service_info(zc, type_, name, "ADDED") + + def update_service(self, zc: Zeroconf, type_: str, name: str) -> None: + self.handle_service_info(zc, type_, name, "UPDATED") + + def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None: + to_log = f"Service {name} removed\n" + to_log += _MDNS_TYPES[type_] + self.logger.warning(to_log) + + def browse_interactive(self) -> None: + zc = Zeroconf() + ServiceBrowser(zc, list(_MDNS_TYPES.keys()), self) + try: + input("Browsing Matter mDNS, press enter to stop\n") + finally: + zc.close() diff --git a/src/tools/interop/idt/idt.py b/src/tools/interop/idt/idt.py new file mode 100644 index 00000000000000..b401f88dd8b231 --- /dev/null +++ b/src/tools/interop/idt/idt.py @@ -0,0 +1,196 @@ +# +# 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 argparse +import asyncio +import logging +import os +import shutil +import sys +from pathlib import Path + +from capture import EcosystemController, EcosystemFactory, PacketCaptureRunner, PlatformFactory +from capture.file_utils import border_print, create_file_timestamp, safe_mkdir +from discovery import MatterBleScanner, MatterDnssdListener + +logging.basicConfig( + format='%(asctime)s.%(msecs)03d %(levelname)s {%(module)s} [%(funcName)s]\n%(message)s \n', + level=logging.INFO) + + +class InteropDebuggingTool: + + def __init__(self) -> None: + + self.artifact_dir = None + create_artifact_dir = True + if len(sys.argv) == 1: + create_artifact_dir = False + elif sys.argv[1] != "capture" and sys.argv[1] != "discover": + create_artifact_dir = False + elif len(sys.argv) >= 3 and (sys.argv[2] == "-h" or sys.argv[2] == "--help"): + create_artifact_dir = False + + if not os.environ['IDT_OUTPUT_DIR']: + print('Missing required env vars! Use /scripts!!!') + sys.exit(1) + + self.artifact_dir_parent = os.path.join( + Path(__file__).resolve().parent, + os.environ['IDT_OUTPUT_DIR']) + artifact_timestamp = create_file_timestamp() + self.artifact_dir = os.path.join( + self.artifact_dir_parent, + f'idt_{artifact_timestamp}') + if create_artifact_dir: + safe_mkdir(self.artifact_dir) + border_print(f"Using artifact dir {self.artifact_dir}") + + self.available_platforms = PlatformFactory.list_available_platforms() + self.available_platforms_default = 'Android' if 'Android' in self.available_platforms else None + self.platform_required = self.available_platforms_default is None + + self.available_ecosystems = EcosystemFactory.list_available_ecosystems() + self.available_ecosystems_default = 'ALL' + self.available_ecosystems.append(self.available_ecosystems_default) + + net_interface_path = "/sys/class/net/" + self.available_net_interfaces = os.listdir(net_interface_path) \ + if os.path.exists(net_interface_path) \ + else [] + self.available_net_interfaces.append("any") + self.available_net_interfaces_default = "any" + self.pcap_artifact_dir = os.path.join(self.artifact_dir, "pcap") + self.net_interface_required = self.available_net_interfaces_default is None + + self.ble_artifact_dir = os.path.join(self.artifact_dir, "ble") + self.dnssd_artifact_dir = os.path.join(self.artifact_dir, "dnssd") + + self.process_args() + + def process_args(self) -> None: + parser = argparse.ArgumentParser( + prog="idt", + description="Interop Debugging Tool for Matter") + + subparsers = parser.add_subparsers(title="subcommands") + + discover_parser = subparsers.add_parser( + "discover", help="Discover all Matter devices") + discover_parser.set_defaults(func=self.command_discover) + discover_parser.add_argument( + "--type", + "-t", + help="Specify the type of discovery to execute", + required=True, + choices=[ + "ble", + "b", + "dnssd", + "d"]) + + capture_parser = subparsers.add_parser( + "capture", + help="Capture all information of interest while running a manual test") + + platform_help = "Run capture for a particular platform" + if self.available_platforms_default: + platform_help += f" (default {self.available_platforms_default})" + capture_parser.add_argument("--platform", + "-p", + help=platform_help, + required=self.platform_required, + choices=self.available_platforms, + default=self.available_platforms_default) + + capture_parser.add_argument( + "--ecosystem", + "-e", + help="Run capture for a particular ecosystem or ALL ecosystems (default ALL)", + required=False, + choices=self.available_ecosystems, + default=self.available_ecosystems_default) + + capture_parser.add_argument("--pcap", + "-c", + help="Run packet capture (default t)", + required=False, + choices=['t', 'f'], + default='t') + + interface_help = "Specify packet capture interface" + if self.available_net_interfaces_default: + interface_help += f" (default {self.available_net_interfaces_default})" + capture_parser.add_argument( + "--interface", + "-i", + help=interface_help, + required=self.net_interface_required, + choices=self.available_net_interfaces, + default=self.available_net_interfaces_default) + + capture_parser.set_defaults(func=self.command_capture) + + args, unknown = parser.parse_known_args() + if not hasattr(args, 'func'): + parser.print_help() + else: + args.func(args) + + def command_discover(self, args: argparse.Namespace) -> None: + if args.type[0] == "b": + safe_mkdir(self.ble_artifact_dir) + MatterBleScanner(self.ble_artifact_dir).browse_interactive() + else: + safe_mkdir(self.dnssd_artifact_dir) + MatterDnssdListener(self.dnssd_artifact_dir).browse_interactive() + + def zip_artifacts(self) -> None: + zip_basename = os.path.basename(self.artifact_dir) + archive_file = shutil.make_archive(zip_basename, + 'zip', + root_dir=self.artifact_dir) + output_zip = shutil.move(archive_file, self.artifact_dir_parent) + print(f'Output zip: {output_zip}') + + def command_capture(self, args: argparse.Namespace) -> None: + + pcap = args.pcap == 't' + pcap_runner = None if not pcap else PacketCaptureRunner( + self.pcap_artifact_dir, args.interface) + if pcap: + border_print("Starting pcap") + safe_mkdir(self.pcap_artifact_dir) + pcap_runner.start_pcap() + + asyncio.run(EcosystemFactory.init_ecosystems(args.platform, + args.ecosystem, + self.artifact_dir)) + asyncio.run(EcosystemController.start()) + + border_print("Press enter twice to stop streaming", important=True) + input("") + + if pcap: + border_print("Stopping pcap") + pcap_runner.stop_pcap() + + asyncio.run(EcosystemController.stop()) + asyncio.run(EcosystemController.analyze()) + + border_print("Compressing artifacts, this may take some time!") + self.zip_artifacts() diff --git a/src/tools/interop/idt/requirements.txt b/src/tools/interop/idt/requirements.txt new file mode 100644 index 00000000000000..fa7d89f7cd3206 --- /dev/null +++ b/src/tools/interop/idt/requirements.txt @@ -0,0 +1,3 @@ +zeroconf==0.74.0 +bleak==0.21.1 +mobly==1.12.2 diff --git a/src/tools/interop/idt/res/plugin_demo/ecosystem/demo_ext_ecosystem/__init__.py b/src/tools/interop/idt/res/plugin_demo/ecosystem/demo_ext_ecosystem/__init__.py new file mode 100644 index 00000000000000..a91fcfe2f739d9 --- /dev/null +++ b/src/tools/interop/idt/res/plugin_demo/ecosystem/demo_ext_ecosystem/__init__.py @@ -0,0 +1,22 @@ +# +# 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. +# + +from .demo_ext_ecosystem import DemoExtEcosystem + +__all__ = [ + 'DemoExtEcosystem' +] diff --git a/src/tools/interop/idt/res/plugin_demo/ecosystem/demo_ext_ecosystem/demo_ext_ecosystem.py b/src/tools/interop/idt/res/plugin_demo/ecosystem/demo_ext_ecosystem/demo_ext_ecosystem.py new file mode 100644 index 00000000000000..11fe43f0204332 --- /dev/null +++ b/src/tools/interop/idt/res/plugin_demo/ecosystem/demo_ext_ecosystem/demo_ext_ecosystem.py @@ -0,0 +1,35 @@ +# +# 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. +# + +from capture.base import EcosystemCapture + + +class DemoExtEcosystem(EcosystemCapture): + + def __init__(self, platform, artifact_dir: str) -> None: + self.artifact_dir = artifact_dir + self.platform = platform + self.message = "in the demo external ecosystem" + + async def start_capture(self) -> None: + print("Start capture " + self.message) + + async def stop_capture(self) -> None: + print("Stop capture " + self.message) + + async def analyze_capture(self) -> None: + print("Analyze capture " + self.message) diff --git a/src/tools/interop/idt/scripts/activate.sh b/src/tools/interop/idt/scripts/activate.sh new file mode 100644 index 00000000000000..8b3469cb0acdc4 --- /dev/null +++ b/src/tools/interop/idt/scripts/activate.sh @@ -0,0 +1,25 @@ +# +# 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. +# + +sudo docker run \ + -it \ + --mount source="$PWD"/idt,target=/idt,type=bind \ + --privileged \ + -v /dev/bus/usb:/dev/bus/usb \ + -v /var/run/dbus:/var/run/dbus \ + --net=host \ + idt diff --git a/src/tools/interop/idt/scripts/alias.sh b/src/tools/interop/idt/scripts/alias.sh new file mode 100644 index 00000000000000..9ecbc869e4ab78 --- /dev/null +++ b/src/tools/interop/idt/scripts/alias.sh @@ -0,0 +1,50 @@ +# +# 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. +# + +if [[ $SHELL == "/bin/zsh" ]]; then + echo "idt using zsh config" + export IDT_SRC_PARENT="$(dirname "$0")/../.." +else + echo "idt using bash config" + export IDT_SRC_PARENT="$(dirname "${BASH_SOURCE[0]:-$0}")/../.." +fi + +export IDT_OUTPUT_DIR="IDT_ARTIFACTS" + +alias idt_dir="echo \"idt dir $IDT_SRC_PARENT\"" +idt_dir +alias idt_go="cd \"$IDT_SRC_PARENT\"" + +alias idt_activate="idt_go && source idt/scripts/activate.sh" +alias idt_bootstrap="idt_go && source idt/scripts/bootstrap.sh" +alias idt_build="idt_go && source idt/scripts/build.sh" +alias idt_clean="idt_go && source idt/scripts/clean.sh" +alias idt_connect="idt_go && source idt/scripts/connect.sh" +alias idt_fetch_artifacts="idt_go && source idt/scripts/fetch_artifacts.sh" +alias idt_prune_docker="idt_go && source idt/scripts/prune_docker.sh" +alias idt_push="idt_go && source idt/scripts/push.sh" +alias idt_vars="idt_go && source idt/scripts/vars.sh" +alias idt_clean_artifacts="idt_go && source idt/scripts/clean_artifacts.sh" +alias idt_clean_all="idt_go && source idt/scripts/clean_all.sh" +alias idt_create_vars="idt_go && source idt/scripts/create_vars.sh" + +alias idt="idt_go && \ +if [ -z $PYTHONPYCACHEPREFIX ]; then export PYTHONPYCACHEPREFIX=$IDT_SRC_PARENT/idt/pycache; fi && \ +if [ -z $VIRTUAL_ENV]; then source idt/scripts/py_venv.sh; fi && \ +python3 idt " + +echo "idt commands available! type idt and press tab twice to see available commands." diff --git a/src/tools/interop/idt/scripts/bootstrap.sh b/src/tools/interop/idt/scripts/bootstrap.sh new file mode 100644 index 00000000000000..234798787b0655 --- /dev/null +++ b/src/tools/interop/idt/scripts/bootstrap.sh @@ -0,0 +1,19 @@ +# +# 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. +# + +sudo apt-get update +sudo apt-get install -y docker.io diff --git a/src/tools/interop/idt/scripts/build.sh b/src/tools/interop/idt/scripts/build.sh new file mode 100644 index 00000000000000..6b97c9aa99e781 --- /dev/null +++ b/src/tools/interop/idt/scripts/build.sh @@ -0,0 +1,18 @@ +# +# 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. +# + +sudo docker build idt -t idt diff --git a/src/tools/interop/idt/scripts/clean.sh b/src/tools/interop/idt/scripts/clean.sh new file mode 100644 index 00000000000000..056532db4e4108 --- /dev/null +++ b/src/tools/interop/idt/scripts/clean.sh @@ -0,0 +1,24 @@ +# +# 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. +# + +source idt/scripts/vars.sh +if [[ "$(hostname)" == "$PIHOST" ]]; then + echo "Target env detected, cleaning" + sudo rm -R idt +else + echo "Not in the target env, so not cleaning" +fi diff --git a/src/tools/interop/idt/scripts/clean_all.sh b/src/tools/interop/idt/scripts/clean_all.sh new file mode 100644 index 00000000000000..94b9521cfbc92a --- /dev/null +++ b/src/tools/interop/idt/scripts/clean_all.sh @@ -0,0 +1,22 @@ +# +# 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. +# + +cd idt +rm -R venv/ +rm -R pycache/ +sudo rm -R IDT_ARTIFACTS/ +cd .. diff --git a/src/tools/interop/idt/scripts/clean_artifacts.sh b/src/tools/interop/idt/scripts/clean_artifacts.sh new file mode 100644 index 00000000000000..d6a0a3a01f49cf --- /dev/null +++ b/src/tools/interop/idt/scripts/clean_artifacts.sh @@ -0,0 +1,20 @@ +# +# 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. +# + +cd idt +sudo rm -R "$IDT_OUTPUT_DIR" +cd .. diff --git a/src/tools/interop/idt/scripts/connect.sh b/src/tools/interop/idt/scripts/connect.sh new file mode 100644 index 00000000000000..05d232628d851a --- /dev/null +++ b/src/tools/interop/idt/scripts/connect.sh @@ -0,0 +1,19 @@ +# +# 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. +# + +source idt/scripts/vars.sh +ssh "$PIUSER@$PIHOST" diff --git a/src/tools/interop/idt/scripts/create_vars.sh b/src/tools/interop/idt/scripts/create_vars.sh new file mode 100644 index 00000000000000..44d7a4228150b6 --- /dev/null +++ b/src/tools/interop/idt/scripts/create_vars.sh @@ -0,0 +1,22 @@ +# +# 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. +# + +read -p "Enter RPi host name " pihost +read -p "Enter RPi user name " piuser + +echo "export PIHOST=\"$pihost\"" >"$IDT_SRC_PARENT"/idt/scripts/vars.sh +echo "export PIUSER=\"$piuser\"" >>"$IDT_SRC_PARENT"/idt/scripts/vars.sh diff --git a/src/tools/interop/idt/scripts/fetch_artifacts.sh b/src/tools/interop/idt/scripts/fetch_artifacts.sh new file mode 100644 index 00000000000000..49e90274cf2415 --- /dev/null +++ b/src/tools/interop/idt/scripts/fetch_artifacts.sh @@ -0,0 +1,22 @@ +# +# 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. +# + +source idt/scripts/vars.sh +idt_go +scp -r "$PIUSER@$PIHOST:/home/$PIUSER/idt/$IDT_OUTPUT_DIR" . +cd "$IDT_OUTPUT_DIR" +ls diff --git a/src/tools/interop/idt/scripts/prune_docker.sh b/src/tools/interop/idt/scripts/prune_docker.sh new file mode 100644 index 00000000000000..19a44c2041352c --- /dev/null +++ b/src/tools/interop/idt/scripts/prune_docker.sh @@ -0,0 +1,18 @@ +# +# 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. +# + +sudo docker system prune -a diff --git a/src/tools/interop/idt/scripts/push.sh b/src/tools/interop/idt/scripts/push.sh new file mode 100644 index 00000000000000..5a3d9295e0e4bd --- /dev/null +++ b/src/tools/interop/idt/scripts/push.sh @@ -0,0 +1,45 @@ +# +# 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. +# + +source idt/scripts/vars.sh +if [[ -d idt/venv ]]; then + echo "TEMP MOVING venv" + mv idt/venv TEMPvenv +fi +if [[ -d "idt/$IDT_OUTPUT_DIR" ]]; then + echo "TEMP MOVING IDT_OUTPUT_DIR" + mv "idt/$IDT_OUTPUT_DIR" "TEMP""$IDT_OUTPUT_DIR" +fi +if [[ -d idt/pycache ]]; then + echo "TEMP moving pycache" + mv idt/pycache TEMPpycache +fi + +scp -r ./idt/* "$PIUSER@$PIHOST:/home/$PIUSER"/idt + +if [[ -d TEMPvenv ]]; then + mv TEMPvenv idt/venv + echo "venv restored" +fi +if [[ -d "TEMP"$IDT_OUTPUT_DIR ]]; then + mv "TEMP""$IDT_OUTPUT_DIR" "idt/$IDT_OUTPUT_DIR" + echo "IDT_OUTPUT_DIR restored" +fi +if [[ -d "idt/$IDT_OUTPUT_DIR" ]]; then + echo "pycache restored" + mv TEMPpycache idt/pycache +fi diff --git a/src/tools/interop/idt/scripts/py_venv.sh b/src/tools/interop/idt/scripts/py_venv.sh new file mode 100644 index 00000000000000..3105cbdd391116 --- /dev/null +++ b/src/tools/interop/idt/scripts/py_venv.sh @@ -0,0 +1,26 @@ +# +# 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. +# + +cd idt +if [ -d venv ]; then + source venv/bin/activate +else + python3 -m venv venv + source venv/bin/activate + pip install -r requirements.txt +fi +cd .. diff --git a/src/tools/interop/idt/scripts/setup_shell.sh b/src/tools/interop/idt/scripts/setup_shell.sh new file mode 100644 index 00000000000000..48eb0c925393dd --- /dev/null +++ b/src/tools/interop/idt/scripts/setup_shell.sh @@ -0,0 +1,19 @@ +# +# 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. +# + +echo "source $PWD/idt/scripts/alias.sh" >>~/.bashrc +echo "source $PWD/idt/scripts/alias.sh" >>~/.zshrc diff --git a/src/tools/interop/idt/scripts/vars.sh b/src/tools/interop/idt/scripts/vars.sh new file mode 100644 index 00000000000000..5133e8d726c977 --- /dev/null +++ b/src/tools/interop/idt/scripts/vars.sh @@ -0,0 +1,19 @@ +# +# 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. +# + +export PIHOST="pi-host" +export PIUSER="pi-user"