722 lines
26 KiB
Python
722 lines
26 KiB
Python
"""
|
||
go_ios Runner Module
|
||
|
||
Provides Python functions to interact with the go_ios command line tool
|
||
for running iOS xctest bundles like Fastbot and WebDriverAgent.
|
||
"""
|
||
import logging
|
||
import os
|
||
import platform
|
||
import subprocess
|
||
from typing import Optional, Dict, List, Tuple
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def get_ios_executable() -> str:
|
||
"""
|
||
Get the path to the platform-specific ios executable.
|
||
|
||
Automatically detects the current platform (macOS, Linux, Windows)
|
||
and returns the appropriate executable path from the bin directory.
|
||
|
||
:return: Absolute path to the ios executable
|
||
:raises FileNotFoundError: If the executable for the current platform is not found
|
||
"""
|
||
# Get the bin directory path relative to this file
|
||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||
bin_dir = os.path.join(current_dir, "bin")
|
||
|
||
# Detect current platform and architecture
|
||
system = platform.system().lower()
|
||
machine = platform.machine().lower()
|
||
|
||
# Map platform and architecture to executable name
|
||
if system == "darwin":
|
||
# macOS - check for arm64 (Apple Silicon) or x86_64
|
||
if machine in ("arm64", "aarch64"):
|
||
executable_name = "ios-darwin-arm64"
|
||
else:
|
||
# For Intel Macs, we might need to use arm64 with Rosetta
|
||
# or add an x86_64 binary if available
|
||
executable_name = "ios-darwin-arm64"
|
||
logger.warning(
|
||
f"No native x86_64 macOS binary found, using arm64 binary "
|
||
f"(may require Rosetta 2)"
|
||
)
|
||
elif system == "linux":
|
||
executable_name = "ios-linux-amd64"
|
||
elif system == "windows":
|
||
executable_name = "ios-windows-amd64.exe"
|
||
else:
|
||
raise FileNotFoundError(
|
||
f"Unsupported platform: {system}. "
|
||
f"Supported platforms: darwin (macOS), linux, windows"
|
||
)
|
||
|
||
executable_path = os.path.join(bin_dir, executable_name)
|
||
|
||
if not os.path.exists(executable_path):
|
||
raise FileNotFoundError(
|
||
f"iOS executable not found at: {executable_path}. "
|
||
f"Please ensure the go_ios binaries are installed."
|
||
)
|
||
|
||
# Ensure the executable has proper permissions on Unix systems
|
||
if system != "windows":
|
||
if not os.access(executable_path, os.X_OK):
|
||
logger.info(f"Setting executable permission for: {executable_path}")
|
||
os.chmod(executable_path, 0o755)
|
||
|
||
return executable_path
|
||
|
||
|
||
def run_xctest(
|
||
bundle_id: str,
|
||
test_runner_bundle_id: str,
|
||
udid: str,
|
||
xctest_config: str,
|
||
env_vars: Optional[Dict[str, str]] = None,
|
||
ios_executable: Optional[str] = None,
|
||
timeout: Optional[int] = None,
|
||
background: bool = False
|
||
) -> Tuple[Optional[subprocess.Popen], Optional[str], Optional[int]]:
|
||
"""
|
||
Run an iOS xctest bundle using go_ios.
|
||
|
||
:param bundle_id: Bundle ID of the test app (e.g., com.tpshos.FastbotRunner.xctrunner)
|
||
:param test_runner_bundle_id: Bundle ID of the test runner
|
||
:param udid: Device UDID
|
||
:param xctest_config: XCTest configuration name (e.g., FastbotRunner.xctest)
|
||
:param env_vars: Optional dictionary of environment variables to pass to the test
|
||
:param ios_executable: Optional path to ios executable (auto-detected if not provided)
|
||
:param timeout: Optional timeout in seconds (only used if background=False)
|
||
:param background: If True, run in background and return Popen object
|
||
:return: Tuple of (Popen object or None, stdout or None, return code or None)
|
||
When background=True: (Popen, None, None)
|
||
When background=False: (None, stdout, returncode)
|
||
"""
|
||
if ios_executable is None:
|
||
ios_executable = get_ios_executable()
|
||
|
||
# Build the command
|
||
cmd = [
|
||
ios_executable,
|
||
"runtest",
|
||
f"--bundle-id={bundle_id}",
|
||
f"--test-runner-bundle-id={test_runner_bundle_id}",
|
||
f"--udid={udid}",
|
||
f"--xctest-config={xctest_config}",
|
||
]
|
||
|
||
# Add environment variables
|
||
if env_vars:
|
||
for key, value in env_vars.items():
|
||
cmd.extend(["--env", f"{key}={value}"])
|
||
|
||
logger.info(f"Running xctest command: {' '.join(cmd)}")
|
||
|
||
try:
|
||
if background:
|
||
# Run in background, return Popen object for later management
|
||
process = subprocess.Popen(
|
||
cmd,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.STDOUT,
|
||
text=True
|
||
)
|
||
logger.info(f"Started xctest in background with PID: {process.pid}")
|
||
return process, None, None
|
||
else:
|
||
# Run synchronously and wait for completion
|
||
result = subprocess.run(
|
||
cmd,
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=timeout
|
||
)
|
||
if result.returncode != 0:
|
||
logger.error(f"xctest failed with return code {result.returncode}")
|
||
logger.error(f"stderr: {result.stderr}")
|
||
logger.error(f"stdout: {result.stdout}")
|
||
return None, result.stdout + result.stderr, result.returncode
|
||
except subprocess.TimeoutExpired as e:
|
||
logger.error(f"xctest timed out after {timeout} seconds")
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Failed to run xctest: {e}")
|
||
raise
|
||
|
||
|
||
def start_fastbot(
|
||
udid: str,
|
||
target_bundle_id: str,
|
||
duration: int = 10,
|
||
throttle: int = 1000,
|
||
fastbot_bundle_id: str = "com.tpshos.FastbotRunner.xctrunner",
|
||
fastbot_xctest_config: str = "FastbotRunner.xctest",
|
||
ios_executable: Optional[str] = None,
|
||
background: bool = True,
|
||
timeout: Optional[int] = None
|
||
) -> Tuple[Optional[subprocess.Popen], Optional[str], Optional[int]]:
|
||
"""
|
||
Start Fastbot for automated iOS app testing.
|
||
|
||
:param udid: Device UDID (e.g., "00008101-0016601022C0001E")
|
||
:param target_bundle_id: Bundle ID of the target app to test (e.g., "com.apple.AppStore")
|
||
:param duration: Test duration in seconds (default: 10)
|
||
:param throttle: Throttle time in milliseconds between actions (default: 1000)
|
||
:param fastbot_bundle_id: Bundle ID of Fastbot runner (default: com.tpshos.FastbotRunner.xctrunner)
|
||
:param fastbot_xctest_config: XCTest config name (default: FastbotRunner.xctest)
|
||
:param ios_executable: Optional path to ios executable (auto-detected if not provided)
|
||
:param background: If True, run in background (default: True)
|
||
:param timeout: Optional timeout in seconds (only used if background=False)
|
||
:return: Tuple of (Popen object or None, stdout or None, return code or None)
|
||
|
||
Example:
|
||
# Start Fastbot in background
|
||
process, _, _ = start_fastbot(
|
||
udid="00008101-0016601022C0001E",
|
||
target_bundle_id="com.apple.AppStore",
|
||
duration=60,
|
||
throttle=500
|
||
)
|
||
|
||
# Later, stop the process
|
||
process.terminate()
|
||
"""
|
||
env_vars = {
|
||
"BUNDLEID": target_bundle_id,
|
||
"duration": str(duration),
|
||
"throttle": str(throttle),
|
||
}
|
||
|
||
logger.info(
|
||
f"Starting Fastbot for app {target_bundle_id} on device {udid} "
|
||
f"(duration={duration}s, throttle={throttle}ms)"
|
||
)
|
||
|
||
return run_xctest(
|
||
bundle_id=fastbot_bundle_id,
|
||
test_runner_bundle_id=fastbot_bundle_id,
|
||
udid=udid,
|
||
xctest_config=fastbot_xctest_config,
|
||
env_vars=env_vars,
|
||
ios_executable=ios_executable,
|
||
background=background,
|
||
timeout=timeout
|
||
)
|
||
|
||
|
||
def start_wda(
|
||
udid: str,
|
||
port: int = 8100,
|
||
ip: str = "127.0.0.1",
|
||
wda_bundle_id: str = "com.tpshos.WebDriverAgentRunner.xctrunner",
|
||
wda_xctest_config: str = "WebDriverAgentRunner.xctest",
|
||
ios_executable: Optional[str] = None,
|
||
background: bool = True,
|
||
timeout: Optional[int] = None
|
||
) -> Tuple[Optional[subprocess.Popen], Optional[str], Optional[int]]:
|
||
"""
|
||
Start WebDriverAgent (WDA) for iOS device automation.
|
||
|
||
:param udid: Device UDID (e.g., "00008101-0016601022C0001E")
|
||
:param port: Port for WDA server (default: 8100)
|
||
:param ip: IP address for WDA to listen on (default: "127.0.0.1")
|
||
:param wda_bundle_id: Bundle ID of WDA runner (default: com.tpshos.WebDriverAgentRunner.xctrunner)
|
||
:param wda_xctest_config: XCTest config name (default: WebDriverAgentRunner.xctest)
|
||
:param ios_executable: Optional path to ios executable (auto-detected if not provided)
|
||
:param background: If True, run in background (default: True)
|
||
:param timeout: Optional timeout in seconds (only used if background=False)
|
||
:return: Tuple of (Popen object or None, stdout or None, return code or None)
|
||
|
||
Example:
|
||
# Start WDA in background
|
||
process, _, _ = start_wda(
|
||
udid="00008101-0016601022C0001E",
|
||
port=8100
|
||
)
|
||
|
||
# WDA will be accessible at http://127.0.0.1:8100
|
||
|
||
# Later, stop the process
|
||
process.terminate()
|
||
"""
|
||
env_vars = {
|
||
"USE_PORT": str(port),
|
||
"USE_IP": ip,
|
||
}
|
||
|
||
logger.info(
|
||
f"Starting WDA on device {udid} at {ip}:{port}"
|
||
)
|
||
|
||
return run_xctest(
|
||
bundle_id=wda_bundle_id,
|
||
test_runner_bundle_id=wda_bundle_id,
|
||
udid=udid,
|
||
xctest_config=wda_xctest_config,
|
||
env_vars=env_vars,
|
||
ios_executable=ios_executable,
|
||
background=background,
|
||
timeout=timeout
|
||
)
|
||
|
||
|
||
def stop_process(process: subprocess.Popen, timeout: int = 5) -> bool:
|
||
"""
|
||
Gracefully stop a running process.
|
||
|
||
:param process: Popen object to stop
|
||
:param timeout: Timeout in seconds to wait for graceful termination
|
||
:return: True if process was stopped successfully
|
||
"""
|
||
if process is None or process.poll() is not None:
|
||
return True # Already stopped
|
||
|
||
logger.info(f"Stopping process with PID: {process.pid}")
|
||
|
||
try:
|
||
process.terminate()
|
||
process.wait(timeout=timeout)
|
||
return True
|
||
except subprocess.TimeoutExpired:
|
||
logger.warning(f"Process {process.pid} did not terminate gracefully, killing...")
|
||
process.kill()
|
||
process.wait()
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Failed to stop process: {e}")
|
||
return False
|
||
|
||
|
||
def start_port_forward(
|
||
local_port: int,
|
||
device_port: int,
|
||
udid: Optional[str] = None,
|
||
ios_executable: Optional[str] = None,
|
||
background: bool = True
|
||
) -> Tuple[Optional[subprocess.Popen], Optional[str], Optional[int]]:
|
||
"""
|
||
Start port forwarding from local machine to iOS device.
|
||
|
||
:param local_port: Local port to forward from
|
||
:param device_port: Device port to forward to
|
||
:param udid: Optional device UDID (uses first device if not provided)
|
||
:param ios_executable: Optional path to ios executable
|
||
:param background: If True, run in background (default: True)
|
||
:return: Tuple of (Popen object or None, stdout or None, return code or None)
|
||
"""
|
||
if ios_executable is None:
|
||
ios_executable = get_ios_executable()
|
||
|
||
cmd = [ios_executable, "forward", str(local_port), str(device_port)]
|
||
if udid:
|
||
cmd.extend(["--udid", udid])
|
||
|
||
logger.info(f"Starting port forwarding: {local_port} -> {device_port} (UDID: {udid or 'auto'})")
|
||
|
||
try:
|
||
if background:
|
||
process = subprocess.Popen(
|
||
cmd,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.STDOUT,
|
||
text=True
|
||
)
|
||
logger.info(f"Port forwarding started with PID: {process.pid}")
|
||
return process, None, None
|
||
else:
|
||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||
return None, result.stdout + result.stderr, result.returncode
|
||
except Exception as e:
|
||
logger.error(f"Failed to start port forwarding: {e}")
|
||
raise
|
||
|
||
|
||
def start_tunnel(
|
||
ios_executable: Optional[str] = None,
|
||
background: bool = True,
|
||
use_sudo: bool = False,
|
||
sudo_password: Optional[str] = None
|
||
) -> Tuple[Optional[subprocess.Popen], Optional[str], Optional[int]]:
|
||
"""
|
||
Start iOS tunnel (required for device communication).
|
||
|
||
:param ios_executable: Optional path to ios executable
|
||
:param background: If True, run in background (default: True)
|
||
:param use_sudo: If True, run with sudo (may be required on some systems)
|
||
:param sudo_password: Sudo password if use_sudo is True
|
||
:return: Tuple of (Popen object or None, stdout or None, return code or None)
|
||
"""
|
||
if ios_executable is None:
|
||
ios_executable = get_ios_executable()
|
||
|
||
cmd = [ios_executable, "tunnel", "start"]
|
||
|
||
if use_sudo:
|
||
if sudo_password:
|
||
# Use echo to provide password to sudo
|
||
cmd = ["sudo", "-S"] + cmd
|
||
logger.info("Starting tunnel with sudo (password provided)")
|
||
else:
|
||
cmd = ["sudo"] + cmd
|
||
logger.info("Starting tunnel with sudo (will prompt for password)")
|
||
else:
|
||
logger.info("Starting tunnel")
|
||
|
||
try:
|
||
if background:
|
||
if use_sudo and sudo_password:
|
||
# Provide password via stdin
|
||
process = subprocess.Popen(
|
||
cmd,
|
||
stdin=subprocess.PIPE,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.STDOUT,
|
||
text=True
|
||
)
|
||
# Send password
|
||
process.stdin.write(sudo_password + "\n")
|
||
process.stdin.flush()
|
||
else:
|
||
process = subprocess.Popen(
|
||
cmd,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.STDOUT,
|
||
text=True
|
||
)
|
||
logger.info(f"Tunnel started with PID: {process.pid}")
|
||
return process, None, None
|
||
else:
|
||
if use_sudo and sudo_password:
|
||
result = subprocess.run(
|
||
cmd,
|
||
input=sudo_password + "\n",
|
||
capture_output=True,
|
||
text=True
|
||
)
|
||
else:
|
||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||
return None, result.stdout + result.stderr, result.returncode
|
||
except Exception as e:
|
||
logger.error(f"Failed to start tunnel: {e}")
|
||
raise
|
||
|
||
|
||
# Convenience class for managing Fastbot and WDA together
|
||
class GoIOSRunner:
|
||
"""
|
||
High-level class for managing go_ios xctest processes.
|
||
|
||
Provides convenient methods to start/stop Fastbot and WDA,
|
||
with automatic cleanup on destruction.
|
||
|
||
Example:
|
||
runner = GoIOSRunner(udid="00008101-0016601022C0001E")
|
||
|
||
# Start WDA first
|
||
runner.start_wda(port=8100)
|
||
|
||
# Then start Fastbot
|
||
runner.start_fastbot(
|
||
target_bundle_id="com.apple.AppStore",
|
||
duration=60
|
||
)
|
||
|
||
# When done
|
||
runner.stop_all()
|
||
"""
|
||
|
||
def __init__(self, udid: str, ios_executable: Optional[str] = None, wda_url: Optional[str] = None):
|
||
"""
|
||
Initialize GoIOSRunner.
|
||
|
||
:param udid: Device UDID
|
||
:param ios_executable: Optional path to ios executable
|
||
:param wda_url: WDA URL (http://... for Client, UDID/empty for USBClient)
|
||
"""
|
||
self.udid = udid
|
||
self.wda_url = wda_url
|
||
self.ios_executable = ios_executable or get_ios_executable()
|
||
self._wda_process: Optional[subprocess.Popen] = None
|
||
self._fastbot_process: Optional[subprocess.Popen] = None
|
||
self._port_forward_processes: List[subprocess.Popen] = [] # Can have multiple port forwards
|
||
self._tunnel_process: Optional[subprocess.Popen] = None
|
||
|
||
@property
|
||
def is_client_mode(self) -> bool:
|
||
"""判断是否为 Client(HTTP) 连接模式
|
||
|
||
未指定 wda_url 时为了向后兼容默认返回 True 开启端口转发。
|
||
否则只有以 http: 开头才判定为 Client 模式(需要端口转发),
|
||
UDID 或空字符串等则判定为 USBClient 模式(无需端口转发)。
|
||
"""
|
||
if self.wda_url is None:
|
||
return True
|
||
return str(self.wda_url).startswith("http:")
|
||
|
||
def start_wda(
|
||
self,
|
||
port: int = 8100,
|
||
ip: str = "127.0.0.1",
|
||
wda_bundle_id: str = "com.tpshos.WebDriverAgentRunner.xctrunner",
|
||
wda_xctest_config: str = "WebDriverAgentRunner.xctest"
|
||
) -> subprocess.Popen:
|
||
"""
|
||
Start WDA server.
|
||
|
||
:return: Popen object for the WDA process
|
||
"""
|
||
if self._wda_process and self._wda_process.poll() is None:
|
||
logger.warning("WDA is already running, stopping first...")
|
||
self.stop_wda()
|
||
|
||
process, _, _ = start_wda(
|
||
udid=self.udid,
|
||
port=port,
|
||
ip=ip,
|
||
wda_bundle_id=wda_bundle_id,
|
||
wda_xctest_config=wda_xctest_config,
|
||
ios_executable=self.ios_executable,
|
||
background=True
|
||
)
|
||
self._wda_process = process
|
||
return process
|
||
|
||
def start_fastbot(
|
||
self,
|
||
target_bundle_id: str,
|
||
duration: int = 10,
|
||
throttle: int = 1000,
|
||
fastbot_bundle_id: str = "com.tpshos.FastbotRunner.xctrunner",
|
||
fastbot_xctest_config: str = "FastbotRunner.xctest"
|
||
) -> subprocess.Popen:
|
||
"""
|
||
Start Fastbot for automated testing.
|
||
|
||
:return: Popen object for the Fastbot process
|
||
"""
|
||
if self._fastbot_process and self._fastbot_process.poll() is None:
|
||
logger.warning("Fastbot is already running, stopping first...")
|
||
self.stop_fastbot()
|
||
|
||
process, _, _ = start_fastbot(
|
||
udid=self.udid,
|
||
target_bundle_id=target_bundle_id,
|
||
duration=duration,
|
||
throttle=throttle,
|
||
fastbot_bundle_id=fastbot_bundle_id,
|
||
fastbot_xctest_config=fastbot_xctest_config,
|
||
ios_executable=self.ios_executable,
|
||
background=True
|
||
)
|
||
self._fastbot_process = process
|
||
return process
|
||
|
||
def stop_wda(self, timeout: int = 5) -> bool:
|
||
"""Stop WDA server."""
|
||
result = stop_process(self._wda_process, timeout)
|
||
self._wda_process = None
|
||
return result
|
||
|
||
def stop_fastbot(self, timeout: int = 5) -> bool:
|
||
"""Stop Fastbot."""
|
||
result = stop_process(self._fastbot_process, timeout)
|
||
self._fastbot_process = None
|
||
return result
|
||
|
||
def stop_all(self, timeout: int = 5) -> bool:
|
||
"""Stop all running processes."""
|
||
return self.stop_fastbot(timeout) and self.stop_wda(timeout)
|
||
|
||
def is_wda_running(self) -> bool:
|
||
"""Check if WDA is currently running."""
|
||
return self._wda_process is not None and self._wda_process.poll() is None
|
||
|
||
def is_fastbot_running(self) -> bool:
|
||
"""Check if Fastbot is currently running."""
|
||
return self._fastbot_process is not None and self._fastbot_process.poll() is None
|
||
|
||
def start_infrastructure(
|
||
self,
|
||
wda_port: int = 8100,
|
||
fastbot_port: Optional[int] = None,
|
||
use_tunnel: bool = False,
|
||
tunnel_sudo_password: Optional[str] = None
|
||
) -> bool:
|
||
"""
|
||
Start required infrastructure services (tunnel and port forwarding).
|
||
|
||
:param wda_port: Port for WDA (default: 8100)
|
||
:param fastbot_port: Optional port for Fastbot status (e.g., 9197)
|
||
:param use_tunnel: If True, start tunnel (may not be needed on all systems)
|
||
:param tunnel_sudo_password: Sudo password for tunnel if required
|
||
:return: True if all services started successfully
|
||
"""
|
||
try:
|
||
# Start tunnel if requested
|
||
if use_tunnel:
|
||
logger.info("Starting tunnel...")
|
||
process, _, _ = start_tunnel(
|
||
ios_executable=self.ios_executable,
|
||
background=True,
|
||
use_sudo=bool(tunnel_sudo_password),
|
||
sudo_password=tunnel_sudo_password
|
||
)
|
||
self._tunnel_process = process
|
||
import time
|
||
time.sleep(2) # Give tunnel time to start
|
||
|
||
# Start WDA port forwarding if in Client mode
|
||
if self.is_client_mode:
|
||
logger.info(f"Starting WDA port forwarding ({wda_port})...")
|
||
process, _, _ = start_port_forward(
|
||
local_port=wda_port,
|
||
device_port=wda_port,
|
||
udid=self.udid,
|
||
ios_executable=self.ios_executable,
|
||
background=True
|
||
)
|
||
self._port_forward_processes.append(process)
|
||
else:
|
||
logger.info(f"USBClient 模式,跳过 WDA 端口转发 ({wda_port})")
|
||
|
||
# Start Fastbot port forwarding if requested
|
||
if fastbot_port:
|
||
logger.info(f"Starting Fastbot port forwarding ({fastbot_port})...")
|
||
process, _, _ = start_port_forward(
|
||
local_port=fastbot_port,
|
||
device_port=fastbot_port,
|
||
udid=self.udid,
|
||
ios_executable=self.ios_executable,
|
||
background=True
|
||
)
|
||
self._port_forward_processes.append(process)
|
||
|
||
logger.info("Infrastructure services started successfully")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Failed to start infrastructure: {e}")
|
||
self.stop_infrastructure()
|
||
return False
|
||
|
||
def restart_port_forward(self, port: int = 8100) -> bool:
|
||
"""重启端口转发(WDA 恢复时使用)
|
||
|
||
停止所有旧的端口转发进程,然后重建指定端口的转发。
|
||
|
||
:param port: 端口号(默认 8100)
|
||
:return: True 表示重启成功
|
||
"""
|
||
if not self.is_client_mode:
|
||
logger.info(f"USBClient 模式,跳过重启 WDA 端口转发 ({port})")
|
||
return True
|
||
|
||
import time
|
||
|
||
# 停止所有旧的端口转发
|
||
logger.info(f"重启端口转发: 停止旧进程...")
|
||
for process in self._port_forward_processes:
|
||
stop_process(process, timeout=5)
|
||
self._port_forward_processes.clear()
|
||
|
||
time.sleep(1) # 等待端口释放
|
||
|
||
# 重建端口转发
|
||
try:
|
||
logger.info(f"重启端口转发: 建立新的 {port} -> {port} 转发...")
|
||
process, _, _ = start_port_forward(
|
||
local_port=port,
|
||
device_port=port,
|
||
udid=self.udid,
|
||
ios_executable=self.ios_executable,
|
||
background=True
|
||
)
|
||
self._port_forward_processes.append(process)
|
||
logger.info(f"端口转发重启成功 (PID: {process.pid})")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"端口转发重启失败: {e}")
|
||
return False
|
||
|
||
def stop_infrastructure(self, timeout: int = 5) -> bool:
|
||
"""Stop infrastructure services (tunnel and port forwarding)."""
|
||
all_stopped = True
|
||
|
||
# Stop port forwarding processes
|
||
for process in self._port_forward_processes:
|
||
if not stop_process(process, timeout):
|
||
all_stopped = False
|
||
self._port_forward_processes.clear()
|
||
|
||
# Stop tunnel
|
||
if not stop_process(self._tunnel_process, timeout):
|
||
all_stopped = False
|
||
self._tunnel_process = None
|
||
|
||
return all_stopped
|
||
|
||
def __del__(self):
|
||
"""Cleanup on destruction."""
|
||
try:
|
||
self.stop_all()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# Example usage
|
||
import argparse
|
||
|
||
logging.basicConfig(level=logging.INFO)
|
||
|
||
parser = argparse.ArgumentParser(description="go_ios runner utilities")
|
||
parser.add_argument("command", choices=["wda", "fastbot", "info"], help="Command to run")
|
||
parser.add_argument("--udid", required=True, help="Device UDID")
|
||
parser.add_argument("--port", type=int, default=8100, help="WDA port (default: 8100)")
|
||
parser.add_argument("--ip", default="127.0.0.1", help="WDA IP (default: 127.0.0.1)")
|
||
parser.add_argument("--bundle-id", help="Target app bundle ID for Fastbot")
|
||
parser.add_argument("--duration", type=int, default=10, help="Fastbot duration (default: 10)")
|
||
parser.add_argument("--throttle", type=int, default=1000, help="Fastbot throttle (default: 1000)")
|
||
|
||
args = parser.parse_args()
|
||
|
||
if args.command == "info":
|
||
print(f"iOS executable: {get_ios_executable()}")
|
||
elif args.command == "wda":
|
||
print(f"Starting WDA on {args.ip}:{args.port}...")
|
||
process, _, _ = start_wda(
|
||
udid=args.udid,
|
||
port=args.port,
|
||
ip=args.ip,
|
||
background=True
|
||
)
|
||
print(f"WDA started with PID: {process.pid}")
|
||
print("Press Ctrl+C to stop...")
|
||
try:
|
||
process.wait()
|
||
except KeyboardInterrupt:
|
||
process.terminate()
|
||
print("\nWDA stopped.")
|
||
elif args.command == "fastbot":
|
||
if not args.bundle_id:
|
||
parser.error("--bundle-id is required for fastbot command")
|
||
print(f"Starting Fastbot for {args.bundle_id}...")
|
||
process, _, _ = start_fastbot(
|
||
udid=args.udid,
|
||
target_bundle_id=args.bundle_id,
|
||
duration=args.duration,
|
||
throttle=args.throttle,
|
||
background=True
|
||
)
|
||
print(f"Fastbot started with PID: {process.pid}")
|
||
print("Press Ctrl+C to stop...")
|
||
try:
|
||
process.wait()
|
||
except KeyboardInterrupt:
|
||
process.terminate()
|
||
print("\\nFastbot stopped.")
|
||
|