diff --git a/benchkit/devices/adb/__init__.py b/benchkit/devices/adb/__init__.py index 87eedb5..8835deb 100644 --- a/benchkit/devices/adb/__init__.py +++ b/benchkit/devices/adb/__init__.py @@ -8,15 +8,19 @@ import subprocess import sys import time -from typing import Iterable, Optional +from typing import Iterable, Optional, Callable +from benchkit.communication import CommunicationLayer +from benchkit.communication.utils import command_with_env, remote_shell_command +from benchkit.dependencies.dependency import Dependency +from benchkit.dependencies.executables import ExecutableDependency from benchkit.devices.adb.usb import usb_down_up from benchkit.shell.shell import get_args, shell_out -from benchkit.utils.types import Command, PathType +from benchkit.utils.types import Command, Environment, PathType, SplitCommand -def _identifier_from(ip_addr: str, port: int) -> str: - return f"{ip_addr}:{port}" +# def _identifier_from(ip_addr: str, port: int) -> str: +# return f"{ip_addr}:{port}" class ADBError(Exception): @@ -43,23 +47,48 @@ def is_connected(self) -> bool: return "device" == self.status -class AndroidDebugBridge: # TODO add commlayer for "host" +# TODO: investigate the identifier and daemon. temporarily it's mirrored like HDC does it. +class AndroidDebugBridge: """Operations with the phone for high-level adb operations.""" def __init__( self, - ip_addr: str, - port: int = 5555, + # ip_addr: str, + # port: int = 5555, + identifier: str, keep_connected: bool = False, wait_connected: bool = False, expected_os: Optional[str] = None, ) -> None: - self._ip = ip_addr - self._port = port + # self._ip = ip_addr + # self._port = port + self.identifier = identifier self._keep_connected = keep_connected self._wait_connected = wait_connected self._expected_os = expected_os + @staticmethod + def from_device( + device: ADBDevice, + keep_connected: bool = False, + wait_connected: bool = False, + expected_os: Optional[str] = None, + ) -> "AndroidDebugBridge": + return AndroidDebugBridge( + identifier=device.identifier, + keep_connected=keep_connected, + wait_connected=wait_connected, + expected_os=expected_os, + ) + + @staticmethod + def binary() -> str: + return "adb" + + @staticmethod + def dependencies() -> Iterable[Dependency]: + return [ExecutableDependency(AndroidDebugBridge.binary())] + def __enter__(self) -> "AndroidDebugBridge": if not self.is_connected(): self._connect_daemon() @@ -74,14 +103,14 @@ def __exit__(self, exc_type, exc_value, exc_tb) -> None: if not self._keep_connected and self.is_connected(): self._disconnect() - @property - def identifier(self) -> str: - """Get adb identifier of current device. + # @property + # def identifier(self) -> str: + # """Get adb identifier of current device. - Returns: - str: adb identifier of current device. - """ - return _identifier_from(ip_addr=self._ip, port=self._port) + # Returns: + # str: adb identifier of current device. + # """ + # return _identifier_from(ip_addr=self._ip, port=self._port) def is_connected(self) -> bool: """Returns whether the device is connected to adb. @@ -141,7 +170,8 @@ def _connect_daemon(self) -> None: with socket.socket(socket.AF_INET) as conn_sock: conn_sock.settimeout(wait_time) try: - conn_sock.connect((self._ip, self._port)) + # TODO: investigate daemon + # conn_sock.connect((self._ip, self._port)) connected = True except TimeoutError: wait_time *= 2 @@ -167,7 +197,9 @@ def _connect_daemon(self) -> None: raise ADBError("Problem with adb connection") def _connect(self, timeout: int) -> None: - ip_port = f"{self._ip}:{self._port}" + # TODO: investigate daemon + # ip_port = f"{self._ip}:{self._port}" + ip_port = "" succeed = False wait_time = 1 while not succeed: @@ -219,6 +251,19 @@ def _devices() -> Iterable[ADBDevice]: devices = [ADBDevice(*line.split("\t")) for line in device_lines] return devices + + @staticmethod + def query_devices( + filter_callback: Callable[[ADBDevice], bool] = lambda _: True, + ) -> Iterable[ADBDevice]: + """Get filtered list of devices recognized by adb. + + Returns: + Iterable[ADBDevice]: filtered list of devices recognized by adb + """ + devices = AndroidDebugBridge._devices() + filtered = [dev for dev in devices if filter_callback(dev)] + return filtered @staticmethod def _host_shell_out( @@ -440,3 +485,140 @@ def is_installed(self, activity_name: str) -> bool: output = self._target_shell_out(command) is_installed = f"package:{activity_name}" == output.strip() return is_installed + + +class AndroidCommLayer(CommunicationLayer): + def __init__( + self, + bridge: AndroidDebugBridge, + environment: Optional[Environment] = None, + ) -> None: + super().__init__() + self._bridge = bridge + self._additional_environment = environment if environment is not None else {} + self._command_prefix = None + + @property + def remote_host(self) -> Optional[str]: + return self._bridge.identifier + + @property + def is_local(self) -> bool: + return False + + def copy_from_host(self, source: PathType, destination: PathType) -> None: + self._bridge.push(source, destination) + + def copy_to_host(self, source: PathType, destination: PathType) -> None: + self._bridge.pull(source, destination) + + + def _remote_shell_command( + self, + remote_command: Command, + remote_current_dir: PathType | None = None, + ) -> SplitCommand: + dir_args = ["cd", f"{remote_current_dir}", "&&"] if remote_current_dir is not None else [] + command_args = dir_args + get_args(remote_command) + + remote_command = [ + "adb", + "-s", + f"{self._bridge.identifier}", + "shell", + ] + command_args + return remote_command + + def shell( + self, + command: Command, + std_input: str | None = None, + current_dir: PathType | None = None, + environment: Environment = None, + shell: bool = False, + print_input: bool = True, + print_output: bool = True, + print_curdir: bool = True, + timeout: int | None = None, + output_is_log: bool = False, + ignore_ret_codes: Iterable[int] = (), + ignore_any_error_code: bool = False + ) -> str: + env_command = command_with_env( + command=command, + environment=environment, + additional_environment=self._additional_environment, + ) + + full_command = self._remote_shell_command( + remote_command=env_command, + remote_current_dir=current_dir, + ) + + output = shell_out( + command=full_command, + std_input=std_input, + current_dir=None, + print_input=print_input, + print_output=print_output, + timeout=timeout, + output_is_log=output_is_log, + ignore_ret_codes=ignore_ret_codes, + ) + return output + + def pipe_shell( + self, + command: Command, + current_dir: Optional[PathType] = None, + shell: bool = False, + ignore_ret_codes: Iterable[int] = () + ): + raise NotImplementedError("TODO") + + + def background_subprocess( + self, + command: Command, + stdout: PathType, + stderr: PathType, + cwd: PathType | None, + env: dict | None, + establish_new_connection: bool = False + ) -> subprocess.Popen: + dir_args = ["cd", f"{cwd}", "&&"] if cwd is not None else [] + command_args = dir_args + get_args(command) + + adb_command = [ + "adb", + "-s", + f"{self._bridge.identifier}", + "shell", + ] + command_args + + return subprocess.Popen( + adb_command, + stdout=stdout, + stderr=stderr, + env=env, + preexec_fn=os.setsid, + ) + + def path_exists( + self, + path: PathType + ) -> bool: + try: + result = self.shell( + command=f"test -e {path} && echo 1 || echo 0", + output_is_log=False + ) + return bool(int(result.strip())) + except: + return False + + def get_process_status(self, process_handle: subprocess.Popen) -> str: + raise NotImplementedError("TODO") + + def get_process_nb_threads(self, process_handle: subprocess.Popen) -> int: + raise NotImplementedError("TODO") diff --git a/benchkit/devices/hdc/__init__.py b/benchkit/devices/hdc/__init__.py index c091b95..ce0b4f0 100644 --- a/benchkit/devices/hdc/__init__.py +++ b/benchkit/devices/hdc/__init__.py @@ -5,13 +5,17 @@ See HDC documentation: https://docs.openharmony.cn/pages/v5.0/en/application-dev/dfx/hdc.md """ from enum import Enum +import os +import subprocess from platform import system as os_system from typing import Callable, Iterable, List, Optional +from benchkit.communication import CommunicationLayer +from benchkit.communication.utils import command_with_env from benchkit.dependencies.executables import ExecutableDependency from benchkit.dependencies.packages import Dependency from benchkit.shell.shell import get_args, shell_out -from benchkit.utils.types import Command, PathType +from benchkit.utils.types import Command, Environment, PathType class HDCError(Exception): @@ -55,7 +59,6 @@ def __init__( self._keep_connected = keep_connected self._wait_connected = wait_connected self._expected_os = expected_os - self._bin = "hdc.exe" if "Windows" == os_system() else "hdc" @staticmethod def from_device( @@ -72,8 +75,13 @@ def from_device( expected_os=expected_os, ) - def dependencies(self) -> List[Dependency]: - return [ExecutableDependency(self._bin)] + @staticmethod + def binary() -> str: + return "hdc.exe" if "Windows" == os_system() else "hdc" + + @staticmethod + def dependencies() -> List[Dependency]: + return [ExecutableDependency(OpenHarmonyDeviceConnector.binary())] def _find_device(self) -> Optional[HDCDevice]: devices = [dev for dev in self._devices() if dev.identifier == self.identifier] @@ -85,13 +93,15 @@ def _find_device(self) -> Optional[HDCDevice]: case _: raise ValueError("Wrong device list.") - def _devices(self) -> List[HDCDevice]: + @staticmethod + def _devices() -> List[HDCDevice]: """Get list of devices recognized by hdc. Returns: Iterable[HDCDevice]: list of devices recognized by hdc. """ - output = OpenHarmonyDeviceConnector._host_shell_out(command=f"{self._bin} list targets") + binary = OpenHarmonyDeviceConnector.binary() + output = OpenHarmonyDeviceConnector._host_shell_out(command=f"{binary} list targets") device_ids = output.strip().splitlines() devices = [] for dev in device_ids: @@ -111,7 +121,7 @@ def query_devices( Returns: Iterable[HDCDevice]: filtered list of devices recognized by hdc. """ - devices = self._devices() + devices = OpenHarmonyDeviceConnector._devices() filtered = [dev for dev in devices if filter_callback(dev)] return filtered @@ -139,7 +149,7 @@ def _target_shell_out( dir_args = ["cd", f"{current_dir}", "&&"] if current_dir is not None else [] command_args = dir_args + get_args(command) - hdc_command = [f"{self._bin}", "-t", f"{self.identifier}", "shell"] + command_args + hdc_command = [f"{self.binary()}", "-t", f"{self.identifier}", "shell"] + command_args output = shell_out( command=hdc_command, @@ -209,7 +219,7 @@ def pull( local_path (PathType): path where to pull the file on the host. """ command = [ - f"{self._bin}", + f"{self.binary()}", "-t", f"{self.identifier}", "file", @@ -218,3 +228,98 @@ def pull( f"{local_path}", ] self._host_shell_out(command=command) + + +class OpenHarmonyCommLayer(CommunicationLayer): + def __init__( + self, + conn: OpenHarmonyDeviceConnector, + environment: Optional[Environment] = None, + ) -> None: + super().__init__() + self._conn = conn + self._additional_environment = environment if environment is not None else {} + self._command_prefix = None + + @property + def remote_host(self) -> Optional[str]: + return self._conn.identifier + + @property + def is_local(self) -> bool: + return False + + def copy_from_host(self, source: PathType, destination: PathType) -> None: + self._conn.push(source, destination) + + def copy_to_host(self, source: PathType, destination: PathType) -> None: + self._conn.pull(source, destination) + + def shell( + self, + command: Command, + std_input: str | None = None, + current_dir: PathType | None = None, + environment: Environment = None, + shell: bool = False, + print_input: bool = True, + print_output: bool = True, + print_curdir: bool = True, + timeout: int | None = None, + output_is_log: bool = False, + ignore_ret_codes: Iterable[int] = (), + ignore_any_error_code: bool = False + ) -> str: + env_command = command_with_env( + command=command, + environment=environment, + additional_environment=self._additional_environment, + ) + output = self._conn.shell_out( + command=env_command, + current_dir=current_dir, + output_is_log=output_is_log, + ) + return output + + def pipe_shell( + self, + command: Command, + current_dir: Optional[PathType] = None, + shell: bool = False, + ignore_ret_codes: Iterable[int] = () + ): + raise NotImplementedError("TODO") + + def background_subprocess( + self, + command: Command, + stdout: PathType, + stderr: PathType, + cwd: PathType | None, + env: dict | None, + establish_new_connection: bool = False + ) -> subprocess.Popen: + dir_args = ["cd", f"{cwd}", "&&"] if cwd is not None else [] + command_args = dir_args + get_args(command) + + hdc_command = [ + "hdc", + "-t", + f"{self._conn.identifier}", + "shell", + ] + command_args + + return subprocess.Popen( + hdc_command, + stdout=stdout, + stderr=stderr, + env=env, + preexec_fn=os.setsid, + ) + + def get_process_status(self, process_handle: subprocess.Popen) -> str: + raise NotImplementedError("TODO") + + def get_process_nb_threads(self, process_handle: subprocess.Popen) -> int: + raise NotImplementedError("TODO") diff --git a/examples/ipc/ipc.py b/examples/ipc/ipc.py index 2c2d1a2..a32c7ea 100644 --- a/examples/ipc/ipc.py +++ b/examples/ipc/ipc.py @@ -10,7 +10,6 @@ from benchkit.benchmark import Benchmark, CommandAttachment, PostRunHook, PreRunHook from benchkit.campaign import Campaign, CampaignIterateVariables, CampaignSuite from benchkit.commandwrappers import CommandWrapper -from benchkit.devices.hdc import OpenHarmonyDeviceConnector from benchkit.platforms import Platform, get_current_platform from benchkit.sharedlibs import SharedLib from benchkit.utils.dir import caller_dir @@ -23,23 +22,27 @@ class Target(Enum): LOCAL = 1 - MOBILE = 2 - CONTAINER = 3 + HARMONY = 2 + ANDROID = 3 + CONTAINER = 4 + + def is_mobile(self): + return self == Target.HARMONY or self == Target.ANDROID class IPCBenchmark(Benchmark): def __init__( self, bench_dir: PathType, - mobile: bool = False, + local_platform: Platform, + target: Target = Target.LOCAL, skip_rebuild: bool = False, command_wrappers: Iterable[CommandWrapper] = [], command_attachments: Iterable[CommandAttachment] = [], shared_libs: Iterable[SharedLib] = [], pre_run_hooks: Iterable[PreRunHook] = [], post_run_hooks: Iterable[PostRunHook] = [], - platform: Platform | None = None, - hdc: OpenHarmonyDeviceConnector | None = None, + remote_platform: Platform | None = None, ) -> None: super().__init__( command_wrappers=command_wrappers, @@ -47,20 +50,19 @@ def __init__( shared_libs=shared_libs, pre_run_hooks=pre_run_hooks, post_run_hooks=post_run_hooks, + ) + self.target = target self.bench_dir = bench_dir - self.mobile = mobile self.skip_rebuild = skip_rebuild + self.local_platform = local_platform - if platform is not None: - self.platform = platform - - if hdc is not None: - self.hdc = hdc - + if remote_platform is not None: + self.remote_platform = remote_platform + @property def bench_src_path(self) -> pathlib.Path: - return self.bench_dir + return pathlib.Path(self.bench_dir) @staticmethod def get_build_var_names() -> List[str]: @@ -94,21 +96,22 @@ def parse_output_to_results( return parsed def build_bench(self, **kwargs) -> None: - if self.mobile: + if self.target.is_mobile(): return - - self.platform.comm.shell( + + self.local_platform.comm.shell( command="cargo build", current_dir=self.bench_dir, output_is_log=True, ) def clean_bench(self) -> None: - if self.mobile: + if self.target.is_mobile(): + # TODO: remove copied file return if not self.skip_rebuild: - self.platform.comm.shell( + self.local_platform.comm.shell( command="cargo clean", current_dir=self.bench_dir, output_is_log=True, @@ -118,28 +121,28 @@ def single_run(self, m: int, **kwargs) -> str: run_command: List[str] output: str - if self.mobile: + if self.remote_platform: run_command = ["./ipc_runner", "-m", f"{m}"] - # TODO: maybe wrap as well or don't and make comm layer - output = self.hdc.shell_out(run_command, self.bench_dir) - print(output) - else: + output = self.remote_platform.comm.shell(command=run_command, current_dir=self.bench_dir) + return output + else: run_command = ["cargo", "run", "--", "-m", f"{m}"] - wrapped_run_command, wrapped_environment = self._wrap_command( - run_command=run_command, - environment={}, - **kwargs, - ) - output = self.run_bench_command( - environment={}, - run_command=run_command, - wrapped_run_command=wrapped_run_command, - current_dir=self.bench_dir, - wrapped_environment=wrapped_environment, - print_output=True, - ) + wrapped_run_command, wrapped_environment = self._wrap_command( + run_command=run_command, + environment={}, + **kwargs, + ) + + output = self.run_bench_command( + environment={}, + run_command=run_command, + wrapped_run_command=wrapped_run_command, + current_dir=running_directory, + wrapped_environment=wrapped_environment, + print_output=True, + ) return output @@ -147,35 +150,48 @@ def single_run(self, m: int, **kwargs) -> str: def main() -> None: nb_runs = 2 variables = [{"m": 10**i} for i in range(1, 4)] - skip_rebuild = False - target = Target.LOCAL + skip_rebuild = True + target = Target.ANDROID bench_dir: pathlib.Path | str = caller_dir() / "ipc_runner" - platform: Platform | None = None - hdc: OpenHarmonyDeviceConnector | None = None + local_platform: Platform = get_current_platform() + remote_platform: Platform | None = None this_dir = caller_dir() match target: - case Target.LOCAL: - platform = get_current_platform() - case Target.MOBILE: - bench_dir = "/data/testing/ipc/" - hdc = OpenHarmonyDeviceConnector.query_devices(lambda _: True)[0] + case Target.HARMONY: + from benchkit.devices.hdc import OpenHarmonyCommLayer, OpenHarmonyDeviceConnector + + bench_dir = "/data/testing/ipc/ipc_runner" + device = list(OpenHarmonyDeviceConnector.query_devices())[0] + hdc = OpenHarmonyDeviceConnector.from_device(device) + comm = OpenHarmonyCommLayer(hdc) + remote_platform = Platform(comm) + case Target.ANDROID: + from benchkit.devices.adb import AndroidCommLayer, AndroidDebugBridge + + bench_dir = "/data/local/tmp" + device = list(AndroidDebugBridge.query_devices())[0] + adb = AndroidDebugBridge.from_device(device) + comm = AndroidCommLayer(adb) + remote_platform = Platform(comm) case Target.CONTAINER: from rustcontainer import get_rust_docker_platform - platform = get_rust_docker_platform(host_dir=this_dir) + local_platform = get_rust_docker_platform(host_dir=this_dir) bench_dir = "/home/user/workspace/mnt/ipc_runner" benchmark = IPCBenchmark( bench_dir=bench_dir, - platform=platform, - hdc=hdc, - mobile=(Target.MOBILE == target), + local_platform=local_platform, + remote_platform=remote_platform, + target=target, skip_rebuild=skip_rebuild, ) + benchmark._base_data_dir = None + campaign = CampaignIterateVariables( name="IPC Benching", benchmark=benchmark, @@ -184,7 +200,7 @@ def main() -> None: gdb=False, debug=False, constants=None, - enable_data_dir=True, + enable_data_dir=False, ) campaigns: List[Campaign] = [campaign] diff --git a/tests/test_commlayer.py b/tests/test_commlayer.py new file mode 100644 index 0000000..2c04bc4 --- /dev/null +++ b/tests/test_commlayer.py @@ -0,0 +1,51 @@ + +# Copyright (C) 2024 Huawei Technologies Co., Ltd. All rights reserved. +# SPDX-License-Identifier: MIT +""" +Module to test the ADB and HDC commlayer api +""" +from enum import Enum +from benchkit.platforms import Platform, get_current_platform + + +class Target(Enum): + LOCAL = 1 + HARMONY = 2 + ANDROID = 3 + +def main() -> None: + target = Target.ANDROID + platform: Platform + current_dir = "./" + + match target: + case Target.LOCAL: + platform = get_current_platform() + print(platform) + case Target.HARMONY: + from benchkit.devices.hdc import OpenHarmonyDeviceConnector, OpenHarmonyCommLayer + device = list(OpenHarmonyDeviceConnector.query_devices())[0] + hdc = OpenHarmonyDeviceConnector.from_device(device) + comm = OpenHarmonyCommLayer(hdc) + print(device) + platform = Platform(comm) + case Target.ANDROID: + from benchkit.devices.adb import AndroidDebugBridge, AndroidCommLayer + device = list(AndroidDebugBridge.query_devices())[0] + adb = AndroidDebugBridge.from_device(device) + comm = AndroidCommLayer(adb) + print(comm) + platform = Platform(comm) + + output = platform.comm.shell( + command="ls", + current_dir=current_dir, + environment={}, + output_is_log=False, + ) + + print(output) + + +if __name__ == "__main__": + main()