| # Copyright 2024 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from __future__ import annotations |
| |
| import dataclasses |
| import datetime as dt |
| import functools |
| import re |
| from typing import TYPE_CHECKING, Any, Final, Optional, Type |
| |
| from typing_extensions import override |
| |
| from crossbench import path as pth |
| from crossbench.plt.base import Platform |
| from crossbench.plt.device_info import DeviceInfo |
| from crossbench.plt.port_manager import PortManager |
| from crossbench.plt.remote import RemotePlatformMixin |
| from crossbench.plt.version import PlatformVersion |
| |
| if TYPE_CHECKING: |
| from crossbench.plt.base import CPUFreqInfo |
| from crossbench.plt.display_info import DisplayInfo |
| from crossbench.plt.signals import AnySignals |
| from crossbench.types import JsonDict |
| |
| pattern: re.Pattern[str] = re.compile( |
| r"(?P<name>[^\(\)]+) \((?P<version>[0-9\.]+)\) (- Connecting )?" |
| r"\((?P<udid>[0-9A-Z-]+)\)") |
| |
| |
| @dataclasses.dataclass(frozen=True) |
| class IOSDeviceInfo(DeviceInfo): |
| version: str = "" |
| |
| @property |
| def udid(self) -> str: |
| return self.device_id |
| |
| def __str__(self) -> str: |
| return f"{self.name} ({self.version}) ({self.udid})" |
| |
| |
| def ios_devices(platform: Platform, |
| show_all: bool = False) -> dict[str, IOSDeviceInfo]: |
| output = platform.sh_stdout("xcrun", "xctrace", "list", "devices") |
| category_index = 0 |
| results: dict[str, IOSDeviceInfo] = {} |
| for line in output.splitlines(): |
| if line.startswith("== "): |
| category_index += 1 |
| continue |
| if category_index > 1 and not show_all: |
| return results |
| for match in pattern.finditer(line): |
| device = IOSDeviceInfo( |
| match.group("udid"), match.group("name"), match.group("version")) |
| if device.udid in results: |
| raise ValueError("Invalid UDID") |
| results[device.udid] = device |
| return results |
| |
| |
| class IOSPortManager(PortManager): |
| |
| @override |
| def forward(self, local_port: int, remote_port: int) -> int: |
| raise NotImplementedError |
| |
| @override |
| def stop_forward(self, local_port: int) -> None: |
| raise NotImplementedError |
| |
| @override |
| def reverse_forward(self, remote_port: int, local_port: int) -> int: |
| raise NotImplementedError |
| |
| @override |
| def stop_reverse_forward(self, remote_port: int) -> None: |
| raise NotImplementedError |
| |
| |
| # TODO: consider using some abstract MacOS base class. |
| # TODO: consider using https://github.com/facebook/idb |
| # TODO: implement mocked methods |
| # TODO: Follow remove-posix pattern and redirect all shell commands to the |
| # host platform. |
| class IOSPlatform(RemotePlatformMixin, Platform): |
| |
| def __init__(self, |
| host_platform: Platform, |
| device_identifier: Optional[str] = None) -> None: |
| assert not host_platform.is_remote, ( |
| "ios on remote platform is not supported yet") |
| super().__init__(host_platform) |
| self._device: Final[IOSDeviceInfo] = self._find_ios_device( |
| device_identifier) |
| |
| def _find_ios_device( |
| self, device_identifier: Optional[str] = None) -> IOSDeviceInfo: |
| devices: dict[str, IOSDeviceInfo] = ios_devices(self._host_platform) |
| if not devices: |
| raise ValueError("No devices attached.") |
| if not device_identifier: |
| if len(devices) != 1: |
| raise ValueError( |
| f"Too many devices attached, please specify one of: {devices}") |
| return list(devices.values())[0] |
| if device := devices.get(device_identifier): |
| return device |
| matches: list[IOSDeviceInfo] = [] |
| for device in devices.values(): |
| if device_identifier in device.name: |
| matches.append(device) |
| if not matches: |
| raise ValueError( |
| f"No matching device for device identifier: {device_identifier}, " |
| f"choices are {devices}") |
| if len(matches) > 1: |
| raise ValueError( |
| f"Found {len(matches)} devices matching: '{device_identifier}'.\n" |
| f"Choices: {matches}") |
| return matches[0] |
| |
| @override |
| def _create_port_manager(self) -> IOSPortManager: |
| return IOSPortManager(self) |
| |
| @override |
| def _create_default_tmp_dir(self) -> pth.AnyPath: |
| # TODO: temp dir not supported on remote iOS platform |
| return self.path("/var/tmp") # noqa: S108 |
| |
| @override |
| def path(self, path: pth.AnyPathLike) -> pth.AnyPath: |
| return pth.AnyPosixPath(path) |
| |
| @property |
| @override |
| def signals(self) -> Type[AnySignals]: |
| # TODO: Can iOS handle signal? |
| raise NotImplementedError |
| |
| @override |
| def uptime(self) -> dt.timedelta: |
| # TODO: Can we get actual iOS uptime? |
| return dt.timedelta() |
| |
| @functools.lru_cache(maxsize=1) |
| @override |
| def _raw_machine_arch(self) -> str: |
| return "arm64" |
| |
| @property |
| def udid(self) -> str: |
| return self._device.udid |
| |
| @property |
| @override |
| def name(self) -> str: |
| return "ios" |
| |
| @property |
| @override |
| def model(self) -> str: |
| return self._device.name |
| |
| @property |
| @override |
| def cpu(self) -> str: |
| return "ios-arm64" |
| |
| @property |
| @override |
| def version(self) -> PlatformVersion: |
| return PlatformVersion.parse(self.version_str) |
| |
| @property |
| @override |
| def version_str(self) -> str: |
| return self._device.version |
| |
| @functools.lru_cache(maxsize=1) |
| @override |
| def cpu_details(self) -> dict[str, Any]: |
| # TODO: Implement properly (i.e. remove all n/a values) |
| return { |
| "info": self.cpu, |
| "physical cores": self.cpu_cores(logical=False), |
| "logical cores": self.cpu_cores(logical=True), |
| "usage": "n/a", |
| "total usage": "n/a", |
| "system load": "n/a", |
| "min frequency": "n/a", |
| "max frequency": "n/a", |
| "current frequency": "n/a", |
| } |
| |
| @functools.lru_cache(maxsize=1) |
| @override |
| def os_details(self) -> JsonDict: |
| return { |
| "system": "ios", |
| "platform": f"ios {self.version_str}", |
| "version": self.version_str, |
| "release": self.version_str |
| } |
| |
| @functools.lru_cache(maxsize=1) |
| @override |
| def python_details(self) -> JsonDict: |
| return { |
| "version": "n/a", |
| "bits": "n/a", |
| } |
| |
| @override |
| def cpu_cores(self, logical: bool) -> int: #type: ignore[override] |
| return 0 |
| |
| @override |
| def _cpu_freq(self) -> Optional[CPUFreqInfo]: |
| return None |
| |
| def get_relative_cpu_speed(self) -> float: |
| return 1.0 |
| |
| def display_details(self) -> tuple[DisplayInfo, ...]: #type: ignore[override] |
| return () |
| |
| @property |
| @override |
| def is_ios(self) -> bool: |
| return True |
| |
| def _is_safari_app(self, app_or_bin: pth.AnyPathLike) -> bool: |
| return "Safari.app" in pth.AnyPath(app_or_bin).parts |
| |
| @override |
| def search_binary(self, app_or_bin: pth.AnyPathLike) -> Optional[pth.AnyPath]: |
| if self._is_safari_app(app_or_bin): |
| return pth.AnyPath(app_or_bin) |
| raise ValueError( |
| f"Safari is the only supported app on ios, requested {app_or_bin}") |
| |
| @override |
| def is_file(self, path: pth.AnyPathLike) -> bool: |
| if self._is_safari_app(path): |
| return True |
| raise ValueError( |
| f"Safari is the only supported app on ios, requested {path}") |
| |
| @override |
| def app_version(self, app_or_bin: pth.AnyPathLike) -> str: |
| if self._is_safari_app(app_or_bin): |
| return self.version_str |
| raise ValueError( |
| "Safari is the only supported app on ios, requested {app_or_bin}") |
| |
| @override |
| def process_children(self, |
| parent_pid: int, |
| recursive: bool = False) -> list[dict[str, Any]]: |
| return [] |