Skip to content

Commit 84a7a61

Browse files
authored
Launch named iOS simulators (flutter#72323)
1 parent d2d0c73 commit 84a7a61

File tree

11 files changed

+244
-111
lines changed

11 files changed

+244
-111
lines changed

packages/flutter_tools/lib/src/android/android_emulator.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class AndroidEmulator extends Emulator {
142142
Category get category => Category.mobile;
143143

144144
@override
145-
PlatformType get platformType => PlatformType.android;
145+
String get platformDisplay => PlatformType.android.toString();
146146

147147
String _prop(String name) => _properties != null ? _properties[name] : null;
148148

packages/flutter_tools/lib/src/commands/daemon.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,7 +933,7 @@ Map<String, dynamic> _emulatorToMap(Emulator emulator) {
933933
'id': emulator.id,
934934
'name': emulator.name,
935935
'category': emulator.category?.toString(),
936-
'platformType': emulator.platformType?.toString(),
936+
'platformType': emulator.platformDisplay,
937937
};
938938
}
939939

packages/flutter_tools/lib/src/commands/emulators.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class EmulatorsCommand extends FlutterCommand {
2727
final String description = 'List, launch and create emulators.';
2828

2929
@override
30-
final List<String> aliases = <String>['emulator'];
30+
final List<String> aliases = <String>['emulator', 'simulators', 'simulator'];
3131

3232
@override
3333
Future<FlutterCommandResult> runCommand() async {

packages/flutter_tools/lib/src/device.dart

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -614,10 +614,6 @@ abstract class Device {
614614
/// Check if the device is supported by Flutter.
615615
bool isSupported();
616616

617-
// String meant to be displayed to the user indicating if the device is
618-
// supported by Flutter, and, if not, why.
619-
String supportMessage() => isSupported() ? 'Supported' : 'Unsupported';
620-
621617
/// The device's platform.
622618
Future<TargetPlatform> get targetPlatform;
623619

packages/flutter_tools/lib/src/emulator.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ abstract class Emulator {
252252
String get name;
253253
String get manufacturer;
254254
Category get category;
255-
PlatformType get platformType;
255+
String get platformDisplay;
256256

257257
@override
258258
int get hashCode => id.hashCode;
@@ -283,7 +283,7 @@ abstract class Emulator {
283283
emulator.id ?? '',
284284
emulator.name ?? '',
285285
emulator.manufacturer ?? '',
286-
emulator.platformType?.toString() ?? '',
286+
emulator.platformDisplay ?? '',
287287
],
288288
];
289289

packages/flutter_tools/lib/src/ios/ios_emulators.dart

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,26 @@ class IOSEmulators extends EmulatorDiscovery {
1616
bool get canListAnything => globals.iosWorkflow.canListEmulators;
1717

1818
@override
19-
Future<List<Emulator>> get emulators async => getEmulators();
19+
Future<List<Emulator>> get emulators async {
20+
final List<IOSSimulator> simulators = await globals.iosSimulatorUtils.getAvailableDevices();
21+
return simulators.map<Emulator>((IOSSimulator device) {
22+
return IOSEmulator(device);
23+
}).toList();
24+
}
2025

2126
@override
2227
bool get canLaunchAnything => canListAnything;
2328
}
2429

2530
class IOSEmulator extends Emulator {
26-
const IOSEmulator(String id) : super(id, true);
31+
IOSEmulator(IOSSimulator simulator)
32+
: _simulator = simulator,
33+
super(simulator.id, true);
34+
35+
final IOSSimulator _simulator;
2736

2837
@override
29-
String get name => 'iOS Simulator';
38+
String get name => _simulator.name;
3039

3140
@override
3241
String get manufacturer => 'Apple';
@@ -35,43 +44,20 @@ class IOSEmulator extends Emulator {
3544
Category get category => Category.mobile;
3645

3746
@override
38-
PlatformType get platformType => PlatformType.ios;
47+
String get platformDisplay =>
48+
// com.apple.CoreSimulator.SimRuntime.iOS-10-3 => iOS-10-3
49+
_simulator.simulatorCategory?.split('.')?.last ?? 'ios';
3950

4051
@override
4152
Future<void> launch() async {
42-
Future<bool> launchSimulator(List<String> additionalArgs) async {
43-
final List<String> args = <String>[
44-
'open',
45-
...additionalArgs,
46-
'-a',
47-
globals.xcode.getSimulatorPath(),
48-
];
49-
50-
final RunResult launchResult = await globals.processUtils.run(args);
51-
if (launchResult.exitCode != 0) {
52-
globals.printError('$launchResult');
53-
return false;
54-
}
55-
return true;
56-
}
57-
58-
// First run with `-n` to force a device to boot if there isn't already one
59-
if (!await launchSimulator(<String>['-n'])) {
60-
return;
53+
final RunResult launchResult = await globals.processUtils.run(<String>[
54+
'open',
55+
'-a',
56+
globals.xcode.getSimulatorPath(),
57+
]);
58+
if (launchResult.exitCode != 0) {
59+
globals.printError('$launchResult');
6160
}
62-
63-
// Run again to force it to Foreground (using -n doesn't force existing
64-
// devices to the foreground)
65-
await launchSimulator(<String>[]);
61+
return _simulator.boot();
6662
}
6763
}
68-
69-
/// Return the list of iOS Simulators (there can only be zero or one).
70-
List<IOSEmulator> getEmulators() {
71-
final String simulatorPath = globals.xcode.getSimulatorPath();
72-
if (simulatorPath == null) {
73-
return <IOSEmulator>[];
74-
}
75-
76-
return <IOSEmulator>[const IOSEmulator(iosSimulatorId)];
77-
}

packages/flutter_tools/lib/src/ios/simulators.dart

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ class IOSSimulatorUtils {
6666
return <IOSSimulator>[];
6767
}
6868

69-
final List<SimDevice> connected = await _simControl.getConnectedDevices();
69+
final List<SimDevice> connected = (await _simControl.getAvailableDevices())
70+
.where((SimDevice device) => device.isBooted)
71+
.toList();
7072
return connected.map<IOSSimulator>((SimDevice device) {
7173
return IOSSimulator(
7274
device.udid,
@@ -77,6 +79,26 @@ class IOSSimulatorUtils {
7779
);
7880
}).toList();
7981
}
82+
83+
Future<List<IOSSimulator>> getAvailableDevices() async {
84+
if (!_xcode.isInstalledAndMeetsVersionCheck) {
85+
return <IOSSimulator>[];
86+
}
87+
88+
final List<SimDevice> available = await _simControl.getAvailableDevices();
89+
return available
90+
.map<IOSSimulator>((SimDevice device) {
91+
return IOSSimulator(
92+
device.udid,
93+
name: device.name,
94+
simControl: _simControl,
95+
simulatorCategory: device.category,
96+
xcode: _xcode,
97+
);
98+
})
99+
.where((IOSSimulator simulator) => simulator.isSupported())
100+
.toList();
101+
}
80102
}
81103

82104
/// A wrapper around the `simctl` command line tool.
@@ -89,6 +111,23 @@ class SimControl {
89111
_xcode = xcode,
90112
_processUtils = ProcessUtils(processManager: processManager, logger: logger);
91113

114+
/// Create a [SimControl] for testing.
115+
///
116+
/// Defaults to a buffer logger.
117+
@visibleForTesting
118+
factory SimControl.test({
119+
@required ProcessManager processManager,
120+
Logger logger,
121+
Xcode xcode,
122+
}) {
123+
logger ??= BufferLogger.test();
124+
return SimControl(
125+
logger: logger,
126+
xcode: xcode,
127+
processManager: processManager,
128+
);
129+
}
130+
92131
final Logger _logger;
93132
final ProcessUtils _processUtils;
94133
final Xcode _xcode;
@@ -160,10 +199,10 @@ class SimControl {
160199
return devices;
161200
}
162201

163-
/// Returns all the connected simulator devices.
164-
Future<List<SimDevice>> getConnectedDevices() async {
202+
/// Returns all the available simulator devices.
203+
Future<List<SimDevice>> getAvailableDevices() async {
165204
final List<SimDevice> simDevices = await getDevices();
166-
return simDevices.where((SimDevice device) => device.isBooted).toList();
205+
return simDevices.where((SimDevice device) => device.isAvailable).toList();
167206
}
168207

169208
Future<bool> isInstalled(String deviceId, String appId) {
@@ -234,6 +273,17 @@ class SimControl {
234273
return result;
235274
}
236275

276+
Future<RunResult> boot(String deviceId) {
277+
return _processUtils.run(
278+
<String>[
279+
..._xcode.xcrunCommand(),
280+
'simctl',
281+
'boot',
282+
deviceId,
283+
],
284+
);
285+
}
286+
237287
Future<void> takeScreenshot(String deviceId, String outputPath) async {
238288
try {
239289
await _processUtils.run(
@@ -296,7 +346,11 @@ class SimDevice {
296346
final Map<String, dynamic> data;
297347

298348
String get state => data['state']?.toString();
299-
String get availability => data['availability']?.toString();
349+
350+
bool get isAvailable =>
351+
data['isAvailable'] == true ||
352+
data['availability']?.toString() == '(available)';
353+
300354
String get name => data['name']?.toString();
301355
String get udid => data['udid']?.toString();
302356

@@ -394,29 +448,32 @@ class IOSSimulator extends Device {
394448
@override
395449
bool isSupported() {
396450
if (!globals.platform.isMacOS) {
397-
_supportMessage = 'iOS devices require a Mac host machine.';
398451
return false;
399452
}
400453

401454
// Check if the device is part of a blocked category.
402455
// We do not yet support WatchOS or tvOS devices.
403456
final RegExp blocklist = RegExp(r'Apple (TV|Watch)', caseSensitive: false);
404457
if (blocklist.hasMatch(name)) {
405-
_supportMessage = 'Flutter does not support Apple TV or Apple Watch.';
406458
return false;
407459
}
408460
return true;
409461
}
410462

411-
String _supportMessage;
463+
Future<bool> boot() async {
464+
final RunResult result = await _simControl.boot(id);
412465

413-
@override
414-
String supportMessage() {
415-
if (isSupported()) {
416-
return 'Supported';
466+
if (result.exitCode == 0) {
467+
return true;
468+
}
469+
// 149 exit code means the device is already booted. Ignore this error.
470+
if (result.exitCode == 149) {
471+
globals.printTrace('Simulator "$id" already booted.');
472+
return true;
417473
}
418474

419-
return _supportMessage ?? 'Unknown';
475+
globals.logger.printError('$result');
476+
return false;
420477
}
421478

422479
@override

packages/flutter_tools/test/general.shard/android/android_emulator_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ void main() {
7171
expect(emulator.name, displayName);
7272
expect(emulator.manufacturer, manufacturer);
7373
expect(emulator.category, Category.mobile);
74-
expect(emulator.platformType, PlatformType.android);
74+
expect(emulator.platformDisplay, 'android');
7575
});
7676

7777
testWithoutContext('prefers displayname for name', () {

0 commit comments

Comments
 (0)