Files
dotfiles/.agents/skills/ios-simulator-skill/scripts/simctl_boot.py
2026-02-19 00:33:08 -08:00

298 lines
8.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Boot iOS simulators and wait for readiness.
This script boots one or more simulators and optionally waits for them to reach
a ready state. It measures boot time and provides progress feedback.
Key features:
- Boot by UDID or device name
- Wait for device readiness with configurable timeout
- Measure boot performance
- Batch boot operations (boot all, boot by type)
- Progress reporting for CI/CD pipelines
"""
import argparse
import subprocess
import sys
import time
from typing import Optional
from common.device_utils import (
get_booted_device_udid,
list_simulators,
resolve_device_identifier,
)
class SimulatorBooter:
"""Boot iOS simulators with optional readiness waiting."""
def __init__(self, udid: str | None = None):
"""Initialize booter with optional device UDID."""
self.udid = udid
def boot(self, wait_ready: bool = False, timeout_seconds: int = 120) -> tuple[bool, str]:
"""
Boot simulator and optionally wait for readiness.
Args:
wait_ready: Wait for device to be ready before returning
timeout_seconds: Maximum seconds to wait for readiness
Returns:
(success, message) tuple
"""
if not self.udid:
return False, "Error: Device UDID not specified"
start_time = time.time()
# Check if already booted
try:
booted = get_booted_device_udid()
if booted == self.udid:
elapsed = time.time() - start_time
return True, (f"Device already booted: {self.udid} " f"[checked in {elapsed:.1f}s]")
except RuntimeError:
pass # No booted device, proceed with boot
# Execute boot command
try:
cmd = ["xcrun", "simctl", "boot", self.udid]
result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
error = result.stderr.strip()
return False, f"Boot failed: {error}"
except subprocess.TimeoutExpired:
return False, "Boot command timed out"
except Exception as e:
return False, f"Boot error: {e}"
# Optionally wait for readiness
if wait_ready:
ready, wait_message = self._wait_for_ready(timeout_seconds)
elapsed = time.time() - start_time
if ready:
return True, (f"Device booted and ready: {self.udid} " f"[{elapsed:.1f}s total]")
return False, wait_message
elapsed = time.time() - start_time
return True, (
f"Device booted: {self.udid} [boot in {elapsed:.1f}s] "
"(use --wait-ready to wait for availability)"
)
def _wait_for_ready(self, timeout_seconds: int = 120) -> tuple[bool, str]:
"""
Wait for device to reach ready state.
Args:
timeout_seconds: Maximum seconds to wait
Returns:
(success, message) tuple
"""
start_time = time.time()
poll_interval = 0.5
checks = 0
while time.time() - start_time < timeout_seconds:
try:
checks += 1
# Check if device responds to simctl commands
result = subprocess.run(
["xcrun", "simctl", "spawn", self.udid, "launchctl", "list"],
check=False,
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0:
elapsed = time.time() - start_time
return True, (
f"Device ready: {self.udid} " f"[{elapsed:.1f}s, {checks} checks]"
)
except (subprocess.TimeoutExpired, RuntimeError):
pass # Not ready yet
time.sleep(poll_interval)
elapsed = time.time() - start_time
return False, (
f"Boot timeout: Device did not reach ready state "
f"within {elapsed:.1f}s ({checks} checks)"
)
@staticmethod
def boot_all() -> tuple[int, int]:
"""
Boot all available simulators.
Returns:
(succeeded, failed) tuple with counts
"""
simulators = list_simulators(state="available")
succeeded = 0
failed = 0
for sim in simulators:
booter = SimulatorBooter(udid=sim["udid"])
success, _message = booter.boot(wait_ready=False)
if success:
succeeded += 1
else:
failed += 1
return succeeded, failed
@staticmethod
def boot_by_type(device_type: str) -> tuple[int, int]:
"""
Boot all simulators of a specific type.
Args:
device_type: Device type filter (e.g., "iPhone", "iPad")
Returns:
(succeeded, failed) tuple with counts
"""
simulators = list_simulators(state="available")
succeeded = 0
failed = 0
for sim in simulators:
if device_type.lower() in sim["name"].lower():
booter = SimulatorBooter(udid=sim["udid"])
success, _message = booter.boot(wait_ready=False)
if success:
succeeded += 1
else:
failed += 1
return succeeded, failed
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description="Boot iOS simulators and wait for readiness")
parser.add_argument(
"--udid",
help="Device UDID or name (required unless using --all or --type)",
)
parser.add_argument(
"--name",
help="Device name (alternative to --udid)",
)
parser.add_argument(
"--wait-ready",
action="store_true",
help="Wait for device to reach ready state",
)
parser.add_argument(
"--timeout",
type=int,
default=120,
help="Timeout for --wait-ready in seconds (default: 120)",
)
parser.add_argument(
"--all",
action="store_true",
help="Boot all available simulators",
)
parser.add_argument(
"--type",
help="Boot all simulators of a specific type (e.g., iPhone, iPad)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output as JSON",
)
args = parser.parse_args()
# Handle batch operations
if args.all:
succeeded, failed = SimulatorBooter.boot_all()
if args.json:
import json
print(
json.dumps(
{
"action": "boot_all",
"succeeded": succeeded,
"failed": failed,
"total": succeeded + failed,
}
)
)
else:
total = succeeded + failed
print(f"Boot summary: {succeeded}/{total} succeeded, " f"{failed} failed")
sys.exit(0 if failed == 0 else 1)
if args.type:
succeeded, failed = SimulatorBooter.boot_by_type(args.type)
if args.json:
import json
print(
json.dumps(
{
"action": "boot_by_type",
"type": args.type,
"succeeded": succeeded,
"failed": failed,
"total": succeeded + failed,
}
)
)
else:
total = succeeded + failed
print(f"Boot {args.type} summary: {succeeded}/{total} succeeded, " f"{failed} failed")
sys.exit(0 if failed == 0 else 1)
# Resolve device identifier
device_id = args.udid or args.name
if not device_id:
print("Error: Specify --udid, --name, --all, or --type", file=sys.stderr)
sys.exit(1)
try:
udid = resolve_device_identifier(device_id)
except RuntimeError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
# Boot device
booter = SimulatorBooter(udid=udid)
success, message = booter.boot(wait_ready=args.wait_ready, timeout_seconds=args.timeout)
if args.json:
import json
print(
json.dumps(
{
"action": "boot",
"device_id": device_id,
"udid": udid,
"success": success,
"message": message,
}
)
)
else:
print(message)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()