Skip to content

Commit ecd0bb5

Browse files
committed
fix: boot new simulators and sort booted ones first
1 parent 0a1d09e commit ecd0bb5

17 files changed

Lines changed: 2194 additions & 22 deletions

File tree

cli/XCWSimctl.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ NS_ASSUME_NONNULL_BEGIN
55
@interface XCWSimctl : NSObject
66

77
- (nullable NSArray<NSDictionary *> *)listSimulatorsWithError:(NSError * _Nullable * _Nullable)error;
8+
- (nullable NSDictionary *)simulatorCreationOptionsWithError:(NSError * _Nullable * _Nullable)error;
9+
- (nullable NSDictionary *)createSimulatorWithName:(NSString *)name
10+
deviceTypeIdentifier:(NSString *)deviceTypeIdentifier
11+
runtimeIdentifier:(nullable NSString *)runtimeIdentifier
12+
pairedWatchName:(nullable NSString *)pairedWatchName
13+
pairedWatchDeviceTypeIdentifier:(nullable NSString *)pairedWatchDeviceTypeIdentifier
14+
pairedWatchRuntimeIdentifier:(nullable NSString *)pairedWatchRuntimeIdentifier
15+
error:(NSError * _Nullable * _Nullable)error;
816
- (BOOL)bootSimulatorWithUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
917
- (BOOL)shutdownSimulatorWithUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
1018
- (BOOL)toggleAppearanceForSimulatorUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;

cli/XCWSimctl.m

Lines changed: 257 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@
99

1010
@interface XCWSimctl ()
1111

12+
- (nullable NSString *)createSingleSimulatorWithName:(NSString *)name
13+
deviceTypeIdentifier:(NSString *)deviceTypeIdentifier
14+
runtimeIdentifier:(nullable NSString *)runtimeIdentifier
15+
error:(NSError * _Nullable __autoreleasing *)error;
1216
+ (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
1317
error:(NSError * _Nullable __autoreleasing *)error;
1418
+ (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
1519
timeoutSec:(NSTimeInterval)timeoutSec
1620
error:(NSError * _Nullable __autoreleasing *)error;
21+
+ (nullable NSDictionary *)listJSONPayloadWithError:(NSError * _Nullable __autoreleasing *)error;
1722

1823
@end
1924

@@ -34,6 +39,10 @@ + (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
3439
return [value isKindOfClass:[NSString class]] ? value : @"";
3540
}
3641

42+
static NSNumber *XCWNumberValue(id value) {
43+
return [value isKindOfClass:[NSNumber class]] ? value : nil;
44+
}
45+
3746
static NSString *XCWRuntimeDisplayName(NSDictionary *runtime, NSString *runtimeIdentifier) {
3847
NSString *name = XCWStringValue(runtime[@"name"]);
3948
if (name.length > 0 && ![name isEqualToString:runtimeIdentifier]) {
@@ -64,22 +73,8 @@ + (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
6473
@implementation XCWSimctl
6574

6675
- (nullable NSArray<NSDictionary *> *)listSimulatorsWithError:(NSError * _Nullable __autoreleasing *)error {
67-
XCWProcessResult *result = [self.class runSimctl:@[@"list", @"--json"] error:error];
68-
if (result == nil) {
69-
return nil;
70-
}
71-
if (result.terminationStatus != 0) {
72-
if (error != NULL) {
73-
*error = [self.class errorWithDescription:result.stderrString.length > 0 ? result.stderrString : @"simctl list failed" code:1];
74-
}
75-
return nil;
76-
}
77-
78-
NSDictionary *payload = [NSJSONSerialization JSONObjectWithData:result.stdoutData options:0 error:error];
79-
if (![payload isKindOfClass:[NSDictionary class]]) {
80-
if (error != NULL && *error == nil) {
81-
*error = [self.class errorWithDescription:@"Unable to parse simctl JSON output." code:2];
82-
}
76+
NSDictionary *payload = [self.class listJSONPayloadWithError:error];
77+
if (payload == nil) {
8378
return nil;
8479
}
8580

@@ -168,6 +163,230 @@ @implementation XCWSimctl
168163
return flattened;
169164
}
170165

166+
- (nullable NSDictionary *)simulatorCreationOptionsWithError:(NSError * _Nullable __autoreleasing *)error {
167+
NSDictionary *payload = [self.class listJSONPayloadWithError:error];
168+
if (payload == nil) {
169+
return nil;
170+
}
171+
172+
NSArray *runtimeArray = XCWArrayPayload(payload[@"runtimes"], @"runtimes");
173+
NSMutableArray<NSDictionary *> *runtimes = [NSMutableArray array];
174+
NSMutableDictionary<NSString *, NSMutableArray<NSString *> *> *runtimeIdentifiersByDeviceType = [NSMutableDictionary dictionary];
175+
176+
for (NSDictionary *runtime in runtimeArray) {
177+
if (![runtime isKindOfClass:[NSDictionary class]]) {
178+
continue;
179+
}
180+
181+
NSString *identifier = XCWStringValue(runtime[@"identifier"]);
182+
if (identifier.length == 0) {
183+
continue;
184+
}
185+
186+
BOOL isAvailable = [runtime[@"isAvailable"] respondsToSelector:@selector(boolValue)] ? [runtime[@"isAvailable"] boolValue] : YES;
187+
if (!isAvailable) {
188+
continue;
189+
}
190+
191+
NSMutableArray<NSString *> *supportedDeviceTypeIdentifiers = [NSMutableArray array];
192+
NSArray *supportedDeviceTypes = XCWArrayPayload(runtime[@"supportedDeviceTypes"], @"supportedDeviceTypes");
193+
for (NSDictionary *deviceType in supportedDeviceTypes) {
194+
if (![deviceType isKindOfClass:[NSDictionary class]]) {
195+
continue;
196+
}
197+
NSString *deviceTypeIdentifier = XCWStringValue(deviceType[@"identifier"]);
198+
if (deviceTypeIdentifier.length == 0) {
199+
continue;
200+
}
201+
[supportedDeviceTypeIdentifiers addObject:deviceTypeIdentifier];
202+
NSMutableArray<NSString *> *runtimeIdentifiers = runtimeIdentifiersByDeviceType[deviceTypeIdentifier];
203+
if (runtimeIdentifiers == nil) {
204+
runtimeIdentifiers = [NSMutableArray array];
205+
runtimeIdentifiersByDeviceType[deviceTypeIdentifier] = runtimeIdentifiers;
206+
}
207+
[runtimeIdentifiers addObject:identifier];
208+
}
209+
210+
NSMutableDictionary *entry = [NSMutableDictionary dictionary];
211+
entry[@"identifier"] = identifier;
212+
entry[@"name"] = XCWRuntimeDisplayName(runtime, identifier);
213+
entry[@"isAvailable"] = @YES;
214+
entry[@"supportedDeviceTypeIdentifiers"] = supportedDeviceTypeIdentifiers;
215+
216+
NSString *platform = XCWStringValue(runtime[@"platform"]);
217+
if (platform.length > 0) {
218+
entry[@"platform"] = platform;
219+
}
220+
NSString *version = XCWStringValue(runtime[@"version"]);
221+
if (version.length > 0) {
222+
entry[@"version"] = version;
223+
}
224+
NSString *buildVersion = XCWStringValue(runtime[@"buildversion"]);
225+
if (buildVersion.length > 0) {
226+
entry[@"buildVersion"] = buildVersion;
227+
}
228+
229+
[runtimes addObject:entry];
230+
}
231+
232+
NSArray *deviceTypesArray = XCWArrayPayload(payload[@"devicetypes"], @"devicetypes");
233+
NSMutableArray<NSDictionary *> *deviceTypes = [NSMutableArray array];
234+
for (NSDictionary *deviceType in deviceTypesArray) {
235+
if (![deviceType isKindOfClass:[NSDictionary class]]) {
236+
continue;
237+
}
238+
239+
NSString *identifier = XCWStringValue(deviceType[@"identifier"]);
240+
if (identifier.length == 0) {
241+
continue;
242+
}
243+
NSArray<NSString *> *supportedRuntimeIdentifiers = runtimeIdentifiersByDeviceType[identifier];
244+
if (supportedRuntimeIdentifiers.count == 0) {
245+
continue;
246+
}
247+
248+
NSMutableDictionary *entry = [NSMutableDictionary dictionary];
249+
entry[@"identifier"] = identifier;
250+
NSString *name = XCWStringValue(deviceType[@"name"]);
251+
entry[@"name"] = name.length > 0 ? name : identifier;
252+
entry[@"supportedRuntimeIdentifiers"] = supportedRuntimeIdentifiers;
253+
254+
NSString *productFamily = XCWStringValue(deviceType[@"productFamily"]);
255+
if (productFamily.length > 0) {
256+
entry[@"productFamily"] = productFamily;
257+
}
258+
NSString *modelIdentifier = XCWStringValue(deviceType[@"modelIdentifier"]);
259+
if (modelIdentifier.length > 0) {
260+
entry[@"modelIdentifier"] = modelIdentifier;
261+
}
262+
NSString *minRuntimeVersionString = XCWStringValue(deviceType[@"minRuntimeVersionString"]);
263+
if (minRuntimeVersionString.length > 0) {
264+
entry[@"minRuntimeVersionString"] = minRuntimeVersionString;
265+
}
266+
NSString *maxRuntimeVersionString = XCWStringValue(deviceType[@"maxRuntimeVersionString"]);
267+
if (maxRuntimeVersionString.length > 0) {
268+
entry[@"maxRuntimeVersionString"] = maxRuntimeVersionString;
269+
}
270+
NSNumber *minRuntimeVersion = XCWNumberValue(deviceType[@"minRuntimeVersion"]);
271+
if (minRuntimeVersion != nil) {
272+
entry[@"minRuntimeVersion"] = minRuntimeVersion;
273+
}
274+
NSNumber *maxRuntimeVersion = XCWNumberValue(deviceType[@"maxRuntimeVersion"]);
275+
if (maxRuntimeVersion != nil) {
276+
entry[@"maxRuntimeVersion"] = maxRuntimeVersion;
277+
}
278+
279+
[deviceTypes addObject:entry];
280+
}
281+
282+
return @{
283+
@"deviceTypes": deviceTypes,
284+
@"runtimes": runtimes,
285+
};
286+
}
287+
288+
- (nullable NSDictionary *)createSimulatorWithName:(NSString *)name
289+
deviceTypeIdentifier:(NSString *)deviceTypeIdentifier
290+
runtimeIdentifier:(nullable NSString *)runtimeIdentifier
291+
pairedWatchName:(nullable NSString *)pairedWatchName
292+
pairedWatchDeviceTypeIdentifier:(nullable NSString *)pairedWatchDeviceTypeIdentifier
293+
pairedWatchRuntimeIdentifier:(nullable NSString *)pairedWatchRuntimeIdentifier
294+
error:(NSError * _Nullable __autoreleasing *)error {
295+
NSString *primaryUDID = [self createSingleSimulatorWithName:name
296+
deviceTypeIdentifier:deviceTypeIdentifier
297+
runtimeIdentifier:runtimeIdentifier
298+
error:error];
299+
if (primaryUDID.length == 0) {
300+
return nil;
301+
}
302+
303+
NSMutableDictionary *result = [@{
304+
@"udid": primaryUDID,
305+
} mutableCopy];
306+
307+
NSString *watchName = [pairedWatchName stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
308+
NSString *watchDeviceTypeIdentifier = [pairedWatchDeviceTypeIdentifier stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
309+
if (watchName.length == 0 && watchDeviceTypeIdentifier.length == 0) {
310+
return result;
311+
}
312+
if (watchName.length == 0 || watchDeviceTypeIdentifier.length == 0) {
313+
if (error != NULL) {
314+
*error = [self.class errorWithDescription:@"Paired watch creation requires both a watch name and device type." code:23];
315+
}
316+
return nil;
317+
}
318+
319+
NSString *watchUDID = [self createSingleSimulatorWithName:watchName
320+
deviceTypeIdentifier:watchDeviceTypeIdentifier
321+
runtimeIdentifier:pairedWatchRuntimeIdentifier
322+
error:error];
323+
if (watchUDID.length == 0) {
324+
return nil;
325+
}
326+
327+
XCWProcessResult *pairResult = [self.class runSimctl:@[@"pair", watchUDID, primaryUDID]
328+
timeoutSec:120
329+
error:error];
330+
if (pairResult == nil) {
331+
return nil;
332+
}
333+
if (pairResult.terminationStatus != 0) {
334+
if (error != NULL) {
335+
*error = [self.class errorWithDescription:pairResult.stderrString.length > 0 ? pairResult.stderrString : @"Unable to pair watch simulator." code:24];
336+
}
337+
return nil;
338+
}
339+
340+
result[@"pairedWatchUDID"] = watchUDID;
341+
return result;
342+
}
343+
344+
- (nullable NSString *)createSingleSimulatorWithName:(NSString *)name
345+
deviceTypeIdentifier:(NSString *)deviceTypeIdentifier
346+
runtimeIdentifier:(nullable NSString *)runtimeIdentifier
347+
error:(NSError * _Nullable __autoreleasing *)error {
348+
NSString *trimmedName = [name stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
349+
NSString *trimmedDeviceTypeIdentifier = [deviceTypeIdentifier stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
350+
NSString *trimmedRuntimeIdentifier = [runtimeIdentifier stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
351+
if (trimmedName.length == 0) {
352+
if (error != NULL) {
353+
*error = [self.class errorWithDescription:@"Simulator name is required." code:19];
354+
}
355+
return nil;
356+
}
357+
if (trimmedDeviceTypeIdentifier.length == 0) {
358+
if (error != NULL) {
359+
*error = [self.class errorWithDescription:@"Device type identifier is required." code:20];
360+
}
361+
return nil;
362+
}
363+
364+
NSMutableArray<NSString *> *arguments = [@[@"create", trimmedName, trimmedDeviceTypeIdentifier] mutableCopy];
365+
if (trimmedRuntimeIdentifier.length > 0) {
366+
[arguments addObject:trimmedRuntimeIdentifier];
367+
}
368+
369+
XCWProcessResult *result = [self.class runSimctl:arguments timeoutSec:120 error:error];
370+
if (result == nil) {
371+
return nil;
372+
}
373+
if (result.terminationStatus != 0) {
374+
if (error != NULL) {
375+
*error = [self.class errorWithDescription:result.stderrString.length > 0 ? result.stderrString : @"Unable to create simulator." code:21];
376+
}
377+
return nil;
378+
}
379+
380+
NSString *udid = [result.stdoutString stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
381+
if (udid.length == 0) {
382+
if (error != NULL) {
383+
*error = [self.class errorWithDescription:@"simctl create did not return a simulator UDID." code:22];
384+
}
385+
return nil;
386+
}
387+
return udid;
388+
}
389+
171390
- (nullable NSDictionary *)simulatorWithUDID:(NSString *)udid error:(NSError * _Nullable __autoreleasing *)error {
172391
for (NSDictionary *simulator in [self listSimulatorsWithError:error] ?: @[]) {
173392
if ([simulator[@"udid"] isEqualToString:udid]) {
@@ -447,6 +666,28 @@ + (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
447666
return [self runSimctl:arguments timeoutSec:0 error:error];
448667
}
449668

669+
+ (nullable NSDictionary *)listJSONPayloadWithError:(NSError * _Nullable __autoreleasing *)error {
670+
XCWProcessResult *result = [self runSimctl:@[@"list", @"--json"] error:error];
671+
if (result == nil) {
672+
return nil;
673+
}
674+
if (result.terminationStatus != 0) {
675+
if (error != NULL) {
676+
*error = [self errorWithDescription:result.stderrString.length > 0 ? result.stderrString : @"simctl list failed" code:1];
677+
}
678+
return nil;
679+
}
680+
681+
NSDictionary *payload = [NSJSONSerialization JSONObjectWithData:result.stdoutData options:0 error:error];
682+
if (![payload isKindOfClass:[NSDictionary class]]) {
683+
if (error != NULL && *error == nil) {
684+
*error = [self errorWithDescription:@"Unable to parse simctl JSON output." code:2];
685+
}
686+
return nil;
687+
}
688+
return payload;
689+
}
690+
450691
+ (nullable XCWProcessResult *)runSimctl:(NSArray<NSString *> *)arguments
451692
timeoutSec:(NSTimeInterval)timeoutSec
452693
error:(NSError * _Nullable __autoreleasing *)error {

cli/native/XCWNativeBridge.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ void xcw_native_initialize_app(void);
3434
void xcw_native_run_main_loop_slice(double duration_seconds);
3535

3636
char * _Nullable xcw_native_list_simulators(char * _Nullable * _Nullable error_message);
37+
char * _Nullable xcw_native_simulator_creation_options(char * _Nullable * _Nullable error_message);
38+
char * _Nullable xcw_native_create_simulator(const char * _Nonnull name,
39+
const char * _Nonnull device_type_identifier,
40+
const char * _Nullable runtime_identifier,
41+
const char * _Nullable paired_watch_name,
42+
const char * _Nullable paired_watch_device_type_identifier,
43+
const char * _Nullable paired_watch_runtime_identifier,
44+
char * _Nullable * _Nullable error_message);
3745
bool xcw_native_boot_simulator(const char * _Nonnull udid, char * _Nullable * _Nullable error_message);
3846
bool xcw_native_shutdown_simulator(const char * _Nonnull udid, char * _Nullable * _Nullable error_message);
3947
bool xcw_native_toggle_appearance(const char * _Nonnull udid, char * _Nullable * _Nullable error_message);

cli/native/XCWNativeBridge.m

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,44 @@ void xcw_native_run_main_loop_slice(double duration_seconds) {
367367
}
368368
}
369369

370+
char *xcw_native_simulator_creation_options(char **error_message) {
371+
@autoreleasepool {
372+
XCWSimctl *simctl = [[XCWSimctl alloc] init];
373+
NSError *error = nil;
374+
NSDictionary *options = [simctl simulatorCreationOptionsWithError:&error];
375+
if (options == nil) {
376+
XCWSetErrorMessage(error_message, error);
377+
return NULL;
378+
}
379+
return XCWJSONStringFromObject(options, error_message);
380+
}
381+
}
382+
383+
char *xcw_native_create_simulator(const char *name,
384+
const char *device_type_identifier,
385+
const char *runtime_identifier,
386+
const char *paired_watch_name,
387+
const char *paired_watch_device_type_identifier,
388+
const char *paired_watch_runtime_identifier,
389+
char **error_message) {
390+
@autoreleasepool {
391+
XCWSimctl *simctl = [[XCWSimctl alloc] init];
392+
NSError *error = nil;
393+
NSDictionary *result = [simctl createSimulatorWithName:XCWStringFromCString(name)
394+
deviceTypeIdentifier:XCWStringFromCString(device_type_identifier)
395+
runtimeIdentifier:runtime_identifier == NULL ? nil : XCWStringFromCString(runtime_identifier)
396+
pairedWatchName:paired_watch_name == NULL ? nil : XCWStringFromCString(paired_watch_name)
397+
pairedWatchDeviceTypeIdentifier:paired_watch_device_type_identifier == NULL ? nil : XCWStringFromCString(paired_watch_device_type_identifier)
398+
pairedWatchRuntimeIdentifier:paired_watch_runtime_identifier == NULL ? nil : XCWStringFromCString(paired_watch_runtime_identifier)
399+
error:&error];
400+
if (result == nil) {
401+
XCWSetErrorMessage(error_message, error);
402+
return NULL;
403+
}
404+
return XCWJSONStringFromObject(result, error_message);
405+
}
406+
}
407+
370408
bool xcw_native_boot_simulator(const char *udid, char **error_message) {
371409
@autoreleasepool {
372410
return XCWPerformSimctlAction(error_message, ^BOOL(XCWSimctl *simctl, NSError **error) {

0 commit comments

Comments
 (0)