Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Connected iPhones and iPads show up faster, and their device frame loads
without waiting on `devicectl`.
- Android emulators stream straight from the emulator on macOS, like
Android Studio's embedded emulator: a steady 60 fps with less load on
the emulator, sized to the view. Older emulators, ones started without
Expand Down
12 changes: 8 additions & 4 deletions app/lib/src/stream/ios/ios_device_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io';

import 'package:flutter/services.dart';
import 'package:simutil_apple/simutil_apple.dart';
import 'package:simutil_core/simutil_core.dart';

import 'package:simutil_app/src/stream/ios/apple_chrome.dart';
Expand All @@ -20,7 +21,7 @@ class IosDeviceSession implements DeviceSession {
_ensureHandler();
}

/// devicectl identifier.
/// Hardware UDID.
final String udid;

/// Device name, as the capture device reports it.
Expand Down Expand Up @@ -91,8 +92,8 @@ class IosDeviceSession implements DeviceSession {
}))!;
textureId = result['textureId']! as int;
setFrameCounter(this, _framesShown);
// Off the start path: devicectl takes a second or more, and sessions
// start one at a time.
// Off the start path: the model lookup can fall back to devicectl,
// which takes a second or more, and sessions start one at a time.
if (chrome == null) unawaited(_attachChrome());
// Live once the first frame reports its size.
} on PlatformException catch (e) {
Expand All @@ -115,8 +116,11 @@ class IosDeviceSession implements DeviceSession {
return AppleChrome.load(_chromeChannel, udid, model: model);
}

/// Hardware model (`iPhone15,2`) from devicectl.
/// Hardware model (`iPhone15,2`) from lockdownd, or devicectl when
/// usbmuxd does not have the device.
Future<String?> _productType() async {
final values = await lockdownValues(udid);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: The model lookup opens a second usbmuxd session

lockdownValues(udid) runs a fresh ListDevices + Connect + GetValue (5 s timeout each) on every stream start, although IOSDeviceService.getPhysicalDevices already read the same lockdownd values and dropped ProductType from the Device. When the id is not a UDID (the devicectl path can produce a CoreDevice UUID, which lockdownValues never matches) the lookup is a wasted round trip, and in the fallback case it delays the device frame by up to 10 s before devicectl even starts. Carry the model on Device or reuse the values from the listing.

if (values?['ProductType'] case final String type) return type;
final dir = await Directory.systemTemp.createTemp('simutil-device-');
final file = File('${dir.path}/info.json');
try {
Expand Down
4 changes: 4 additions & 0 deletions packages/simutil/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- `simutil launch --camera-back` / `--camera-front` feed an Android emulator's camera from your Mac's webcam, an image or a video file (`webcam0`, `image:qr.png`, `video:clip.mp4`).

### Changed

- Connected iPhones and iPads show up faster: SimUtil asks the Mac's device service directly and only falls back to `devicectl` when that does not answer.

### Fixed

- Headless Android emulators (`simutil launch --headless`) use your computer's GPU, so they run much faster.
Expand Down
9 changes: 9 additions & 0 deletions packages/simutil_apple/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## 1.4.0

- `IOSDeviceService.getPhysicalDevices` asks usbmuxd (`ListDevices`) and
each device's lockdownd (`GetValue`, no pairing session) instead of
running `devicectl`; it falls back to `devicectl list devices` only when
usbmuxd or a device does not answer. Device ids are now hardware UDIDs on
both paths (`hardwareProperties.udid` from `devicectl`), not CoreDevice
identifiers. New optional `listUsbmuxDevices` constructor parameter for
tests.
- `lockdownValues(udid)` reads a connected device's lockdownd values
(`DeviceName`, `ProductType`, `ProductVersion`, ...).
- `IosAutomation.launchWithCamera` grants a simulator app camera access and
relaunches it with a camera injector dylib inserted
(`SIMCTL_CHILD_DYLD_INSERT_LIBRARIES`) and its shared memory name
Expand Down
4 changes: 3 additions & 1 deletion packages/simutil_apple/lib/simutil_apple.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/// Apple simulator and device APIs for SimUtil via simctl and devicectl.
/// Apple simulator and device APIs for SimUtil via simctl, usbmuxd and
/// devicectl.
///
/// Lists iPhone/iPad/Watch/TV simulators and connected hardware, plus
/// Xcode Derived Data helpers, simulator screen recording, and slimming
Expand All @@ -18,5 +19,6 @@ export 'src/simulator_app_converter.dart';
export 'src/simulator_recorder.dart';
export 'src/slim/simulator_slimmer.dart';
export 'src/slim/slim_categories.dart';
export 'src/usbmux.dart' show lockdownValues;
export 'src/xcode_cache_service.dart';
export 'src/ios_automation.dart';
73 changes: 5 additions & 68 deletions packages/simutil_apple/lib/src/apple_device_watch.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:simutil_apple/src/usbmux.dart';
import 'package:simutil_core/simutil_core.dart';

/// CoreSimulator's default device set.
Expand Down Expand Up @@ -38,65 +37,6 @@ Stream<void> watchSimulatorSet(String deviceSet) {
.where((e) => isSimulatorSetChange(e.path.substring(dir.path.length)));
}

/// usbmuxd's socket on macOS.
const usbmuxdSocket = '/var/run/usbmuxd';

/// The `Listen` request that subscribes to usbmuxd attach / detach events
/// (plist protocol: 16-byte little-endian header, then an XML plist).
Uint8List usbmuxListenRequest({int tag = 1}) {
final body = utf8.encode(
'<?xml version="1.0" encoding="UTF-8"?>'
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" '
'"http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
'<plist version="1.0"><dict>'
'<key>MessageType</key><string>Listen</string>'
'<key>ClientVersionString</key><string>simutil</string>'
'<key>ProgName</key><string>simutil</string>'
'<key>kLibUSBMuxVersion</key><integer>3</integer>'
'</dict></plist>',
);
final header = ByteData(16)
..setUint32(0, 16 + body.length, Endian.little)
..setUint32(4, 1, Endian.little) // protocol version: plist
..setUint32(8, 8, Endian.little) // message: plist
..setUint32(12, tag, Endian.little);
return Uint8List.fromList([...header.buffer.asUint8List(), ...body]);
}

/// Splits usbmuxd replies into their `MessageType`s (`Result`, `Attached`,
/// `Detached`, `Paired`).
class UsbmuxDecoder {
final _buffer = BytesBuilder(copy: false);

static final _messageType = RegExp(
r'<key>MessageType</key>\s*<string>([^<]*)</string>',
);

/// Feeds [chunk]; returns the message types of the frames it finished.
List<String> add(List<int> chunk) {
_buffer.add(chunk);
var data = _buffer.takeBytes();
final types = <String>[];
while (data.length >= 16) {
final length = ByteData.sublistView(
data,
0,
4,
).getUint32(0, Endian.little);
if (length < 16) {
data = Uint8List(0); // corrupt: drop and wait for the reconnect
break;
}
if (data.length < length) break;
final plist = utf8.decode(data.sublist(16, length), allowMalformed: true);
types.add(_messageType.firstMatch(plist)?.group(1) ?? '');
data = data.sublist(length);
}
_buffer.add(data);
return types;
}
}

/// One usbmuxd `Listen` connection: emits on every attach / detach, ends
/// when usbmuxd closes the socket.
Stream<void> _usbmuxListenOnce(String socketPath) {
Expand All @@ -105,15 +45,12 @@ Stream<void> _usbmuxListenOnce(String socketPath) {
controller = StreamController<void>(
onListen: () async {
try {
final connected = socket =
await Socket.connect(
InternetAddress(socketPath, type: InternetAddressType.unix),
0,
)
..add(usbmuxListenRequest());
final connected = socket = await connectUsbmuxd(socketPath)
..add(usbmuxRequest('Listen'));
final decoder = UsbmuxDecoder();
await for (final chunk in connected) {
for (final type in decoder.add(chunk)) {
for (final message in decoder.add(chunk)) {
final type = message['MessageType'];
if (type == 'Attached' || type == 'Detached') controller.add(null);
}
}
Expand Down
30 changes: 24 additions & 6 deletions packages/simutil_apple/lib/src/ios_device_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,30 @@ import 'dart:io';

import 'package:path/path.dart' as p;
import 'package:simutil_apple/src/apple_device_watch.dart';
import 'package:simutil_apple/src/usbmux.dart' as usbmux;
import 'package:simutil_core/simutil_core.dart';

/// Apple simulators (`simctl`) and physical devices (`devicectl`).
/// Apple simulators (`simctl`) and physical devices (usbmuxd + lockdownd,
/// falling back to `devicectl`).
///
/// Covers iPhone, iPad, Watch, TV, and other CoreSimulator runtimes.
/// No-ops on non-macOS hosts.
class IOSDeviceService implements DeviceService {
/// Creates a service that runs `xcrun` via [CommandExec].
///
/// [pathExists] overrides the directory check used to locate the
/// simulator app (for tests).
IOSDeviceService(this._exec, {bool Function(String path)? pathExists})
: _pathExists = pathExists ?? _directoryExists;
/// simulator app, and [listUsbmuxDevices] the usbmuxd device lookup
/// (both for tests).
IOSDeviceService(
this._exec, {
bool Function(String path)? pathExists,
Future<List<Device>?> Function()? listUsbmuxDevices,
}) : _pathExists = pathExists ?? _directoryExists,
_listUsbmuxDevices = listUsbmuxDevices ?? usbmux.listUsbmuxDevices;

final CommandExec _exec;
final bool Function(String path) _pathExists;
final Future<List<Device>?> Function() _listUsbmuxDevices;

static bool _directoryExists(String path) => Directory(path).existsSync();

Expand Down Expand Up @@ -258,10 +266,16 @@ class IOSDeviceService implements DeviceService {
);
}

/// Asks usbmuxd and each device's lockdownd first: no process, and no
/// simulators to filter out. `devicectl` (a second or more) only runs when
/// usbmuxd does not answer.
@override
Future<List<Device>> getPhysicalDevices() async {
if (!Platform.isMacOS) return [];
return await _listUsbmuxDevices() ?? await _listDevicectlDevices();
}

Future<List<Device>> _listDevicectlDevices() async {
final tempDirectory = Directory.systemTemp;
final outputFile = File('${tempDirectory.path}/ios_devices.json');

Expand All @@ -282,7 +296,7 @@ class IOSDeviceService implements DeviceService {

return parsePhysicalDevices(json);
} catch (e, st) {
log('IOSDeviceService.getPhysicalDevices error: $e\n$st');
log('IOSDeviceService._listDevicectlDevices error: $e\n$st');
return [];
} finally {
if (outputFile.existsSync()) {
Expand Down Expand Up @@ -322,7 +336,11 @@ class IOSDeviceService implements DeviceService {
map['deviceProperties'] as Map<String, dynamic>? ?? {};
final connectionProps =
map['connectionProperties'] as Map<String, dynamic>? ?? {};
final identifier = map['identifier'] as String? ?? '';
final hardware = map['hardwareProperties'] as Map<String, dynamic>?;
// The hardware UDID, as usbmuxd reports it; `identifier` is a
// CoreDevice UUID.
final identifier =
hardware?['udid'] as String? ?? map['identifier'] as String? ?? '';
final name = deviceProps['name'] as String? ?? '';
final osVersion = deviceProps['osVersionNumber'] as String? ?? '';
if (_deviceReality(map) == 'simulated') continue;
Expand Down
Loading
Loading