/
simulator.js
1761 lines (1555 loc) · 62.6 KB
/
simulator.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Detects iOS developer and distribution certificates and the WWDR certificate.
*
* @module simulator
*
* @copyright
* Copyright (c) 2014-2017 by Appcelerator, Inc. All Rights Reserved.
*
* @license
* Licensed under the terms of the Apache Public License.
* Please see the LICENSE included with this distribution for details.
*/
'use strict';
const appc = require('node-appc');
const async = require('async');
const EventEmitter = require('events').EventEmitter;
const magik = require('./utilities').magik;
const fs = require('fs');
const mkdirp = require('mkdirp');
const net = require('net');
const path = require('path');
const readPlist = require('./utilities').readPlist;
const simctl = require('./simctl');
const spawn = require('child_process').spawn;
const Tail = require('always-tail');
const util = require('util');
const xcode = require('./xcode');
const __ = appc.i18n(__dirname).__;
var cache;
exports.detect = detect;
exports.findSimulators = findSimulators;
exports.launch = launch;
exports.stop = stop;
exports.SimHandle = SimHandle;
exports.SimulatorCrash = SimulatorCrash;
/**
* @class
* @classdesc An exception for when an app crashes in the iOS Simulator.
* @constructor
* @param {Array|Object} [crashFiles] - The crash details.
*/
function SimulatorCrash(crashFiles) {
this.name = 'SimulatorCrash';
this.message = __('App crashed in the iOS Simulator');
this.crashFiles = Array.isArray(crashFiles) ? crashFiles : crashFiles ? [ crashFiles ] : null;
}
SimulatorCrash.prototype = Object.create(Error.prototype);
SimulatorCrash.prototype.constructor = SimulatorCrash;
function SimHandle(obj) {
appc.util.mix(this, obj);
}
const deviceState = exports.deviceState = {
DOES_NOT_EXIST: -1,
CREATING: 0,
SHUTDOWN: 1,
BOOTING: 2,
BOOTED: 3,
SHUTTING_DOWN: 4
};
const deviceStateNames = exports.deviceStateNames = {
0: 'Creating',
1: 'Shutdown',
2: 'Booting',
3: 'Booted',
4: 'Shutting Down'
};
/**
* Helper function for comparing two simulators based on the model name.
*
* @param {Object} a - A simulator handle.
* @param {Object} b - Another simulator handle.
*
* @returns {Number} - Returns -1 if a < b, 1 if a > b, and 0 if they are equal.
*/
function compareSims(a, b) {
return a.model < b.model ? -1 : a.model > b.model ? 1 : 0;
}
/**
* Detects iOS simulators.
*
* @param {Object} [options] - An object containing various settings.
* @param {Boolean} [options.bypassCache=false] - When true, re-detects all iOS simulators.
* @param {Function} [callback(err, results)] - A function to call with the simulator information.
*
* @emits module:simulator#detected
* @emits module:simulator#error
*
* @returns {Handle}
*/
function detect(options, callback) {
return magik(options, callback, function (emitter, options, callback) {
if (cache && !options.bypassCache) {
var dupe = JSON.parse(JSON.stringify(cache));
emitter.emit('detected', dupe);
return callback(null, dupe);
}
function fakeWatchSim(name, udid, model, xcodes) {
return {
udid: udid,
name: name,
version: '1.0',
type: 'watchos',
deviceType: null,
deviceName: name,
deviceDir: null,
model: model,
family: 'watch',
supportsXcode: xcodes,
supportsWatch: {},
watchCompanion: {},
runtime: null,
runtimeName: 'watchOS 1.0',
systemLog: null,
dataDir: null
};
}
var results = {
simulators: {
ios: {},
watchos: {},
crashDir: appc.fs.resolvePath('~/Library/Logs/DiagnosticReports'),
},
issues: []
};
xcode.detect(options, function (err, xcodeInfo) {
if (err) {
emitter.emit('error', err);
return callback(err);
}
var xcodeIds = Object
.keys(xcodeInfo.xcode)
.filter(function (ver) { return xcodeInfo.xcode[ver].supported; })
.sort(function (a, b) {
var v1 = xcodeInfo.xcode[a].version;
var v2 = xcodeInfo.xcode[b].version;
return xcodeInfo.xcode[a].selected || appc.version.lt(v1, v2) ? -1 : appc.version.eq(v1, v2) ? 0 : 1;
});
// if we have Xcode 6.2, 6.3, or 6.4, then inject some fake devices for WatchKit 1.x
xcodeIds.some(function (id) {
var xc = xcodeInfo.xcode[id];
if (appc.version.satisfies(xc.version, '>=6.2 <7.0')) {
var xcodes = {};
xcodeIds.forEach(function (id) {
if (appc.version.satisfies(xcodeInfo.xcode[id].version, '>=6.2 <7.0')) {
xcodes[id] = true;
}
});
results.simulators.watchos['1.0'] = [
fakeWatchSim('Apple Watch - 38mm', '58045222-F0C1-41F7-A4BD-E2EDCFBCF5B9', 'Watch0,1', xcodes),
fakeWatchSim('Apple Watch - 42mm', 'D5C1DA2F-7A74-49C8-809A-906E554021B0', 'Watch0,2', xcodes)
];
return true;
}
});
if (!xcodeInfo.selectedXcode) {
emitter.emit('detected', results);
return callback(null, results);
}
simctl.list({ simctl: xcodeInfo.selectedXcode.executables.simctl }, function (err, info) {
if (err) {
return callback(err);
}
// create a lookup of runtimes from simctl
var simctlRuntimes = {};
info.runtimes.forEach(function (runtime) {
simctlRuntimes[runtime.identifier] = runtime;
});
var coreSimDir = appc.fs.resolvePath('~/Library/Developer/CoreSimulator/Devices');
Object.keys(info.devices).forEach(function (type) {
info.devices[type].forEach(function (device) {
var plist = readPlist(path.join(coreSimDir, device.udid, 'device.plist'));
if (!plist) {
return;
}
xcodeIds.forEach(function (id) {
var xc = xcodeInfo.xcode[id],
deviceType = xc.simDeviceTypes[plist.deviceType],
runtime = simctlRuntimes[plist.runtime];
// This code finds the sim runtime and builds the list of associated
// iOS SDKs which may be different based which Xcode's simctl is run.
// For example, sim runtime 10.3 is associated with iOS 10.3 and 10.3.1.
// Because of this, we define the same simulator for each associated
// iOS SDK version.
if (!runtime) {
runtime = xc.simRuntimes[plist.runtime];
if (runtime) {
runtime.versions = [ runtime.version ];
}
} else {
runtime.versions = [ runtime.version ];
if (xc.simRuntimes[plist.runtime]) {
var ver = xc.simRuntimes[plist.runtime].version;
if (ver !== runtime.version) {
runtime.versions.push(ver);
}
}
}
if (!deviceType || !runtime) {
// wrong xcode, skip
return;
}
var family = deviceType.model.replace(/[\W0-9]/g, '').toLowerCase(),
type = family === 'iphone' || family === 'ipad' ? 'ios' : 'watchos';
// for each runtime iOS SDK version, define the simulator
runtime.versions.forEach(function (runtimeVersion) {
var sim;
results.simulators[type][runtimeVersion] || (results.simulators[type][runtimeVersion] = []);
results.simulators[type][runtimeVersion].some(function (s) {
if (s.udid === plist.UDID) {
sim = s;
return true;
}
});
if (!sim) {
results.simulators[type][runtimeVersion].push(sim = {
udid: plist.UDID,
name: plist.name,
version: runtimeVersion,
type: type,
deviceType: plist.deviceType,
deviceName: deviceType.name,
deviceDir: path.join(coreSimDir, device.udid),
model: deviceType.model,
family: family,
supportsXcode: {},
supportsWatch: {},
watchCompanion: {},
runtime: plist.runtime,
runtimeName: runtime.name,
systemLog: appc.fs.resolvePath('~/Library/Logs/CoreSimulator/' + device.udid + '/system.log'),
dataDir: path.join(coreSimDir, device.udid, 'data')
});
}
sim.supportsXcode[id] = true;
if (type === 'ios') {
sim.supportsWatch[id] = deviceType.supportsWatch;
}
});
});
});
});
// this is pretty nasty, but necessary...
// basically this will populate the watchCompanion property for each iOS Simulator
// so that it makes choosing simulator pairs way easier
Object.keys(results.simulators.ios).forEach(function (iosSimVersion) {
results.simulators.ios[iosSimVersion].forEach(function (iosSim) {
Object.keys(iosSim.supportsWatch).forEach(function (xcodeId) {
if (iosSim.supportsWatch[xcodeId]) {
var xc = xcodeInfo.xcode[xcodeId];
Object.keys(xc.simDevicePairs).forEach(function (iOSRange) {
if (appc.version.satisfies(iosSim.version, iOSRange)) {
Object.keys(xc.simDevicePairs[iOSRange]).forEach(function (watchOSRange) {
Object.keys(results.simulators.watchos).forEach(function (watchosSDK) {
results.simulators.watchos[watchosSDK].forEach(function (watchSim) {
if (appc.version.satisfies(watchSim.version, watchOSRange)) {
iosSim.watchCompanion[xcodeId] || (iosSim.watchCompanion[xcodeId] = {});
iosSim.watchCompanion[xcodeId][watchSim.udid] = watchSim;
}
});
});
});
}
});
}
});
});
});
// sort the simulators
['ios', 'watchos'].forEach(function (type) {
Object.keys(results.simulators[type]).forEach(function (ver) {
results.simulators[type][ver].sort(compareSims);
});
});
// the cache must be a clean copy that we'll clone for subsequent detect() calls
// because we can't allow the cache to be modified by reference
cache = JSON.parse(JSON.stringify(results));
emitter.emit('detected', results);
callback(null, results);
});
});
});
};
/**
* Finds the specified app's bundle identifier. If a watch app name is specified,
* then it will attempt to find the watch app's bundle identifier.
*
* @param {String} appPath - The path to the compiled .app directory
* @param {String|Boolean} [watchAppName] - The name of the watch app to find. If value is true, then it will choose the first watch app.
*
* @returns {Object} An object containing the app's id and if `watchAppName` is specified, the watch app's id, OS version, and min OS version.
*/
function getAppInfo(appPath, watchAppName) {
// validate the specified appPath
if (!fs.existsSync(appPath)) {
throw new Error(__('App path does not exist: ' + appPath));
}
// get the app's id
var infoPlist = path.join(appPath, 'Info.plist');
if (!fs.existsSync(infoPlist)) {
throw new Error(__('Unable to find Info.plist in root of specified app path: ' + infoPlist));
}
var plist = readPlist(infoPlist);
if (!plist || !plist.CFBundleIdentifier) {
throw new Error(__('Failed to parse app\'s Info.plist: ' + infoPlist));
}
var results = {
appId: plist.CFBundleIdentifier,
appName: path.basename(appPath).replace(/\.app$/, '')
};
if (watchAppName) {
// look for WatchKit v1 apps
var pluginsDir = path.join(appPath, 'PlugIns');
fs.existsSync(pluginsDir) && fs.readdirSync(pluginsDir).some(function (name) {
var extDir = path.join(pluginsDir, name);
if (fs.existsSync(extDir) && fs.statSync(extDir).isDirectory() && /\.appex$/.test(name)) {
return fs.readdirSync(extDir).some(function (name) {
var appDir = path.join(extDir, name);
if (fs.existsSync(appDir) && fs.statSync(appDir).isDirectory() && /\.app$/.test(name)) {
var plist = readPlist(path.join(appDir, 'Info.plist'));
if (plist && plist.WKWatchKitApp && (typeof watchAppName !== 'string' || fs.existsSync(path.join(appDir, watchAppName)))) {
results.watchAppName = path.basename(appDir).replace(/\.app$/, '');
results.watchAppId = plist.CFBundleIdentifier;
results.watchOSVersion = '1.0';
results.watchMinOSVersion = '1.0';
return true;
}
}
});
}
});
if (!results.watchAppId) {
// look for WatchKit v2 apps
var watchDir = path.join(appPath, 'Watch');
fs.existsSync(watchDir) && fs.readdirSync(watchDir).some(function (name) {
var plist = readPlist(path.join(watchDir, name, 'Info.plist'));
if (plist && (plist.DTPlatformName === 'watchos' || plist.WKWatchKitApp) && (typeof watchAppName !== 'string' || fs.existsSync(path.join(watchDir, watchAppName)))) {
results.watchAppName = name.replace(/\.app$/, '');
results.watchAppId = plist.CFBundleIdentifier;
results.watchOSVersion = plist.DTPlatformVersion;
results.watchMinOSVersion = plist.MinimumOSVersion;
return true;
}
});
}
if (!results.watchAppId) {
if (typeof watchAppName === 'string') {
throw new Error(__('Unable to find a watch app named "%s".', watchAppName));
} else {
throw new Error(__('The launch watch app flag was set, however unable to find a watch app.'));
}
}
}
return results;
}
/**
* Finds a iOS Simulator and/or Watch Simulator as well as the supported Xcode based on the specified options.
*
* @param {Object} [options] - An object containing various settings.
* @param {String} [options.appBeingInstalled] - The path to the iOS app to install after launching the iOS Simulator.
* @param {Boolean} [options.bypassCache=false] - When true, re-detects Xcode and all simulators.
* @param {Function} [options.logger] - A function to log debug messages to.
* @param {String} [options.iosVersion] - The iOS version of the app so that ioslib picks the appropriate Xcode.
* @param {String} [options.minIosVersion] - The minimum iOS SDK to detect.
* @param {String} [options.minWatchosVersion] - The minimum watchOS SDK to detect.
* @param {String|Array<String>} [options.searchPath] - One or more path to scan for Xcode installations.
* @param {String|SimHandle} simHandleOrUDID - A iOS sim handle or the UDID of the iOS Simulator to launch or null if you want ioslib to pick one.
* @param {String} [options.simType=iphone] - The type of simulator to launch. Must be either "iphone" or "ipad". Only applicable when udid is not specified.
* @param {String} [options.simVersion] - The iOS version to boot. Defaults to the most recent version.
* @param {String} [options.supportedVersions] - A string with a version number or range to check if an Xcode install is supported.
* @param {Boolean} [options.watchAppBeingInstalled] - The id of the watch app. Required in order to find a watch simulator.
* @param {String} [options.watchHandleOrUDID] - A watch sim handle or UDID of the Watch Simulator to launch or null if your app has a watch app and you want ioslib to pick one.
* @param {String} [options.watchMinOSVersion] - The min Watch OS version supported by the specified watch app id.
* @param {Function} callback(err, simHandle, watchSimHandle, selectedXcode, simInfo, xcodeInfo) - A function to call with the simulators found.
*/
function findSimulators(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
} else if (typeof options !== 'object') {
options = {};
}
typeof callback === 'function' || (callback = function () {});
// detect xcodes
xcode.detect(options, function (err, xcodeInfo) {
if (err) {
return callback(err);
}
// detect the simulators
detect(options, function (err, simInfo) {
if (err) {
return callback(err);
}
var logger = typeof options.logger === 'function' ? options.logger : function () {},
simHandle = options.simHandleOrUDID instanceof SimHandle ? options.simHandleOrUDID : null,
watchSimHandle = options.watchHandleOrUDID instanceof SimHandle ? options.watchHandleOrUDID : null,
selectedXcode;
if (options.simHandleOrUDID) {
// validate the udid
if (!(options.simHandleOrUDID instanceof SimHandle)) {
var vers = Object.keys(simInfo.simulators.ios);
logger(__('Validating iOS Simulator UDID %s', options.simHandleOrUDID));
for (var i = 0, l = vers.length; !simHandle && i < l; i++) {
var sims = simInfo.simulators.ios[vers[i]];
for (var j = 0, k = sims.length; j < k; j++) {
if (sims[j].udid === options.simHandleOrUDID) {
logger(__('Found iOS Simulator UDID %s', options.simHandleOrUDID));
simHandle = new SimHandle(sims[j]);
break;
}
}
}
if (!simHandle) {
return callback(new Error(__('Unable to find an iOS Simulator with the UDID "%s".', options.simHandleOrUDID)));
}
}
if (options.minIosVersion && appc.version.lt(simHandle.version, options.minIosVersion)) {
return callback(new Error(__('The selected iOS %s Simulator is less than the minimum iOS version %s.', simHandle.version, options.minIosVersion)));
}
if (options.watchAppBeingInstalled) {
var xcodeId = Object
.keys(simHandle.watchCompanion)
.filter(function (xcodeId) {
return xcodeInfo.xcode[xcodeId].supported;
})
.sort(function (a, b) {
var v1 = xcodeInfo.xcode[a].version;
var v2 = xcodeInfo.xcode[b].version;
return xcodeInfo.xcode[a].selected || appc.version.lt(v1, v2) ? -1 : appc.version.eq(v1, v2) ? 0 : 1;
})
.pop();
if (!xcodeId) {
return callback(new Error(__('Unable to find any watchOS Simulators that can be paired with the specified iOS Simulator %s.', simHandle.udid)));
}
if (options.watchHandleOrUDID) {
if (!(options.watchHandleOrUDID instanceof SimHandle)) {
logger(__('Watch app present, validating Watch Simulator UDID %s', options.watchHandleOrUDID));
Object.keys(simInfo.simulators.watchos).some(function (ver) {
return simInfo.simulators.watchos[ver].some(function (sim) {
if (sim.udid === options.watchHandleOrUDID) {
logger(__('Found Watch Simulator UDID %s', options.watchHandleOrUDID));
watchSimHandle = new SimHandle(sim);
return true;
}
});
});
if (!watchSimHandle) {
return callback(new Error(__('Unable to find a Watch Simulator with the UDID "%s".', options.watchHandleOrUDID)));
}
}
} else {
logger(__('Watch app present, autoselecting a Watch Simulator'));
var companions = simHandle.watchCompanion[xcodeId];
Object.keys(companions)
.sort(function (a, b) {
return companions[a].model < companions[b].model ? 1 : companions[a].model > companions[b].model ? -1 : 0;
})
.some(function (watchUDID) {
watchSimHandle = new SimHandle(companions[watchUDID]);
return true;
});
if (!watchSimHandle) {
return callback(new Error(__('Specified iOS Simulator "%s" does not support Watch apps.', options.simHandleOrUDID)));
}
}
selectedXcode = xcodeInfo.xcode[xcodeId];
} else {
// no watch sim, just an ios sim
// find the version of Xcode
var xcodeId = Object
.keys(simHandle.supportsXcode)
.filter(function (id) {
if (!simHandle.supportsXcode[id]) {
return false;
}
if (!xcodeInfo.xcode[id].supported) {
return false;
}
if (options.iosVersion && xcodeInfo.xcode[id].sdks.indexOf(options.iosVersion) === -1) {
return false;
}
return true;
})
.sort(function (a, b) {
var v1 = xcodeInfo.xcode[a].version;
var v2 = xcodeInfo.xcode[b].version;
return xcodeInfo.xcode[a].selected || appc.version.lt(v1, v2) ? -1 : appc.version.eq(v1, v2) ? 0 : 1;
})
.pop();
selectedXcode = xcodeInfo.xcode[xcodeId];
}
if (!selectedXcode) {
if (options.iosVersion) {
return callback(new Error(__('Unable to find any Xcode installations that support both iOS %s and iOS Simulator %s.', options.iosVersion, options.simHandleOrUDID)));
} else if (options.minIosVersion) {
return callback(new Error(__('Unable to find any Xcode installations that support at least iOS %s and iOS Simulator %s.', options.minIosVersion, options.simHandleOrUDID)));
} else {
return callback(new Error(__('Unable to find any supported Xcode installations. Please install the latest Xcode.')));
}
}
if (watchSimHandle && !simHandle.watchCompanion[selectedXcode.version + ':' + selectedXcode.build][watchSimHandle.udid]) {
return callback(new Error(__('Specified Watch Simulator "%s" is not compatible with iOS Simulator "%s".', watchSimHandle.udid, simHandle.udid)));
}
if (options.watchAppBeingInstalled && !options.watchHandleOrUDID && !watchSimHandle) {
if (options.watchMinOSVersion) {
return callback(new Error(__('Unable to find a Watch Simulator that supports watchOS %s.', options.watchMinOSVersion)));
} else {
return callback(new Error(__('Unable to find a Watch Simulator.')));
}
}
logger(__('Selected iOS Simulator: %s', simHandle.name));
logger(__(' UDID = %s', simHandle.udid));
logger(__(' iOS = %s', simHandle.version));
if (watchSimHandle) {
if (options.watchAppBeingInstalled && options.watchHandleOrUDID) {
logger(__('Selected watchOS Simulator: %s', watchSimHandle.name));
} else {
logger(__('Autoselected watchOS Simulator: %s', watchSimHandle.name));
}
logger(__(' UDID = %s', watchSimHandle.udid));
logger(__(' watchOS = %s', watchSimHandle.version));
}
logger(__('Autoselected Xcode: %s', selectedXcode.version));
} else {
logger(__('No iOS Simulator UDID specified, searching for best match'));
if (options.watchAppBeingInstalled && options.watchHandleOrUDID) {
logger(__('Validating Watch Simulator UDID %s', options.watchHandleOrUDID));
Object.keys(simInfo.simulators.watchos).some(function (ver) {
return simInfo.simulators.watchos[ver].some(function (sim) {
if (sim.udid === options.watchHandleOrUDID) {
watchSimHandle = new SimHandle(sim);
logger(__('Found Watch Simulator UDID %s', options.watchHandleOrUDID));
return true;
}
});
});
if (!watchSimHandle) {
return callback(new Error(__('Unable to find a Watch Simulator with the UDID "%s".', options.watchHandleOrUDID)));
}
}
// pick one
var xcodeIds = Object
.keys(xcodeInfo.xcode)
.filter(function (ver) {
if (!xcodeInfo.xcode[ver].supported) {
return false;
}
if (watchSimHandle && !watchSimHandle.supportsXcode[ver]) {
return false;
}
if (options.iosVersion && xcodeInfo.xcode[ver].sdks.indexOf(options.iosVersion) === -1) {
return false;
}
return true;
})
.sort(function (a, b) {
var v1 = xcodeInfo.xcode[a].version;
var v2 = xcodeInfo.xcode[b].version;
return xcodeInfo.xcode[a].selected || appc.version.lt(v1, v2) ? -1 : appc.version.eq(v1, v2) ? 0 : 1;
})
.reverse();
if (xcodeIds.length) {
logger(__('Scanning Xcodes: %s', xcodeIds.join(' ')));
// loop through xcodes
for (var i = 0; !simHandle && i < xcodeIds.length; i++) {
var xcodeId = xcodeIds[i],
xc = xcodeInfo.xcode[xcodeId],
simVers = xc.sims.sort().reverse();
logger(__('Scanning Xcode %s sims: %s', xcodeIds[i], simVers.join(', ')));
// loop through each xcode simulators
for (var j = 0; !simHandle && j < simVers.length; j++) {
if ((!options.simVersion || simVers[j] === options.simVersion) &&
(!options.minIosVersion || appc.version.gte(simVers[j], options.minIosVersion)) &&
simInfo.simulators.ios[simVers[j]]
) {
var sims = simInfo.simulators.ios[simVers[j]].sort(compareSims).reverse();
// loop through each simulator
for (var k = 0; !simHandle && k < sims.length; k++) {
if (!options.simType || sims[k].family === options.simType) {
// if we're installing a watch extension, make sure we pick a simulator that supports the watch
if (options.watchAppBeingInstalled) {
if (watchSimHandle) {
Object.keys(sims[k].supportsWatch).forEach(function (xcodeVer) {
if (watchSimHandle.supportsXcode[xcodeVer]) {
selectedXcode = xcodeInfo.xcode[xcodeVer];
simHandle = new SimHandle(sims[k]);
return true;
}
});
} else if (sims[k].supportsWatch[xcodeIds[i]]) {
// make sure this version of Xcode has a watch simulator that supports the watch app version
xc.watchos.sims.some(function (ver) {
if (appc.version.gte(ver, options.watchMinOSVersion) && simInfo.simulators.watchos[ver]) {
simHandle = new SimHandle(sims[k]);
selectedXcode = xcodeInfo.xcode[xcodeIds[i]];
watchSimHandle = new SimHandle(simInfo.simulators.watchos[ver].sort(compareSims).reverse()[0]);
return true;
}
});
}
} else {
// no watch app
logger(__('No watch app being installed, so picking first Simulator'));
simHandle = new SimHandle(sims[k]);
// fallback to the newest supported Xcode version
xcodeIds.some(function (id) {
if (simHandle.supportsXcode[id]) {
selectedXcode = xcodeInfo.xcode[id];
return true;
}
});
}
}
}
}
}
}
}
if (!selectedXcode) {
if (options.iosVersion && watchSimHandle) {
return callback(new Error(__('Unable to find any Xcode installations that supports iOS Simulator %s and watchOS Simulator %s.', options.iosVersion, watchSimHandle.udid)));
} else if (options.iosVersion) {
return callback(new Error(__('Unable to find any Xcode installations that supports iOS Simulator %s.', options.iosVersion)));
} else if (options.minIosVersion) {
return callback(new Error(__('Unable to find any Xcode installations that supports at least iOS %s.', options.minIosVersion)));
} else {
return callback(new Error(__('Unable to find any supported Xcode installations. Please install the latest Xcode.')));
}
}
if (!simHandle) {
// user experience!
if (options.simVersion) {
return callback(new Error(__('Unable to find an iOS Simulator running iOS %s.', options.simVersion)));
} else {
return callback(new Error(__('Unable to find an iOS Simulator.')));
}
} else if (options.watchAppBeingInstalled && !watchSimHandle) {
return callback(new Error(__('Unable to find a watchOS Simulator that supports watchOS %s', options.watchMinOSVersion)));
}
logger(__('Autoselected iOS Simulator: %s', simHandle.name));
logger(__(' UDID = %s', simHandle.udid));
logger(__(' iOS = %s', simHandle.version));
if (watchSimHandle) {
if (options.watchAppBeingInstalled && options.watchHandleOrUDID) {
logger(__('Selected watchOS Simulator: %s', watchSimHandle.name));
} else {
logger(__('Autoselected watchOS Simulator: %s', watchSimHandle.name));
}
logger(__(' UDID = %s', watchSimHandle.udid));
logger(__(' watchOS = %s', watchSimHandle.version));
}
logger(__('Autoselected Xcode: %s', selectedXcode.version));
}
simHandle.simulator = selectedXcode.executables.simulator;
simHandle.simctl = selectedXcode.executables.simctl;
if (watchSimHandle) {
watchSimHandle.simulator = selectedXcode.executables.watchsimulator;
watchSimHandle.simctl = selectedXcode.executables.simctl;
}
callback(null, simHandle, watchSimHandle, selectedXcode, simInfo, xcodeInfo);
});
});
}
/**
* Launches the specified iOS Simulator or picks one automatically.
*
* @param {String|SimHandle} simHandleOrUDID - A iOS sim handle or the UDID of the iOS Simulator to launch or null if you want ioslib to pick one.
* @param {Object} [options] - An object containing various settings.
* @param {String} [options.appPath] - The path to the iOS app to install after launching the iOS Simulator.
* @param {Boolean} [options.autoExit=false] - When "appPath" has been specified, causes the iOS Simulator to exit when the autoExitToken has been emitted to the log output.
* @param {String} [options.autoExitToken=AUTO_EXIT] - A string to watch for to know when to quit the iOS simulator when "appPath" has been specified.
* @param {Boolean} [options.bypassCache=false] - When true, re-detects Xcode and all simulators.
* @param {Boolean} [options.focus=true] - Focus the iOS Simulator after launching. Overrides the "hide" option.
* @param {Boolean} [options.hide=false] - Hide the iOS Simulator after launching. Useful for testing. Ignored if "focus" option is set to true.
* @param {String} [options.iosVersion] - The iOS version of the app so that ioslib picks the appropriate Xcode.
* @param {Boolean} [options.killIfRunning] - Kill the iOS Simulator if already running.
* @param {String} [options.launchBundleId] - Launches a specific app when the simulator loads. When installing an app, defaults to the app's id unless `launchWatchApp` is set to true.
* @param {Boolean} [options.launchWatchApp=false] - When true, launches the specified app's watch app on an external display and the main app.
* @param {Boolean} [options.launchWatchAppOnly=false] - When true, launches the specified app's watch app on an external display and not the main app.
* @param {String} [options.logFilename] - The name of the log file to search for in the iOS Simulator's "Documents" folder. This file is created after the app is started.
* @param {Number} [options.logServerPort] - The TCP port to connect to get log messages.
* @param {String} [options.minIosVersion] - The minimum iOS SDK to detect.
* @param {String} [options.minWatchosVersion] - The minimum watchOS SDK to detect.
* @param {String|Array<String>} [options.searchPath] - One or more path to scan for Xcode installations.
* @param {String} [options.simType=iphone] - The type of simulator to launch. Must be either "iphone" or "ipad". Only applicable when udid is not specified.
* @param {String} [options.simVersion] - The iOS version to boot. Defaults to the most recent version.
* @param {String} [options.supportedVersions] - A string with a version number or range to check if an Xcode install is supported.
* @param {Boolean} [options.uninstallApp=false] - When true and `appPath` is specified, uninstalls the app before installing the new app. If app is not installed already, it continues.
* @param {String} [options.watchAppName] - The name of the watch app to install. If omitted, automatically picks the watch app.
* @param {String} [options.watchHandleOrUDID] - A watch sim handle or the UDID of the Watch Simulator to launch or null if your app has a watch app and you want ioslib to pick one.
* @param {Function} [callback(err, simHandle)] - A function to call when the simulator has launched.
*
* @emits module:simulator#app-quit
* @emits module:simulator#app-started
* @emits module:simulator#error
* @emits module:simulator#exit
* @emits module:simulator#launched
* @emits module:simulator#log
* @emits module:simulator#log-debug
* @emits module:simulator#log-error
* @emits module:simulator#log-file
* @emits module:simulator#log-raw
*
* @returns {Handle}
*/
function launch(simHandleOrUDID, options, callback) {
return magik(options, callback, function (emitter, options, callback) {
emitter.stop = function () {}; // for stopping logging
if (!options.appPath && (options.launchWatchApp || options.launchWatchAppOnly)) {
var err = new Error(
options.launchWatchAppOnly
? __('You must specify an appPath when launchWatchApp is true.')
: __('You must specify an appPath when launchWatchAppOnly is true.')
);
emitter.emit('error', err);
return callback(err);
}
if (options.logServerPort && (typeof options.logServerPort !== 'number' || options.logServerPort < 1 || options.logServerPort > 65535)) {
var err = new Error(__('Log server port must be a number between 1 and 65535'));
emitter.emit('error', err);
return callback(err);
}
var appId,
watchAppId,
findSimOpts = appc.util.mix({
simHandleOrUDID: simHandleOrUDID,
logger: function (msg) {
emitter.emit('log-debug', msg);
}
}, options);
if (options.appPath) {
findSimOpts.appBeingInstalled = true;
try {
var ids = getAppInfo(options.appPath, options.watchAppName || !!options.launchWatchApp || !!options.launchWatchAppOnly);
if (!options.launchBundleId) {
appId = ids.appId;
}
if (ids.watchAppId) {
watchAppId = ids.watchAppId;
if (findSimOpts) {
findSimOpts.watchAppBeingInstalled = true;
findSimOpts.watchMinOSVersion = ids.watchMinOSVersion;
}
emitter.emit('log-debug', __('Found watchOS %s app: %s', ids.watchOSVersion, watchAppId));
}
} catch (ex) {
emitter.emit('error', ex);
return callback(ex);
}
} else if (options.launchBundleId) {
appId = options.launchBundleId;
}
findSimulators(findSimOpts, function (err, simHandle, watchSimHandle, selectedXcode, detectedSimInfo) {
if (err) {
emitter.emit('error', err);
return callback(err);
}
if (!selectedXcode.eulaAccepted) {
var eulaErr = new Error(__('Xcode must be launched and the EULA must be accepted before a simulator can be launched.'));
emitter.emit('error', eulaErr);
return callback(eulaErr);
}
var crashFileRegExp,
existingCrashes = getCrashes(),
findLogTimer = null,
logFileTail;
if (options.appPath) {
crashFileRegExp = new RegExp('^' + ids.appName + '_\\d{4}\\-\\d{2}\\-\\d{2}\\-\\d{6}_.*\.crash$'),
simHandle.appName = ids.appName;
watchSimHandle && (watchSimHandle.appName = ids.watchAppName);
}
// sometimes the simulator doesn't remove old log files in which case we get
// our logging jacked - we need to remove them before running the simulator
if (options.logFilename && simHandle.dataDir) {
(function walk(dir) {
var logFile = path.join(dir, 'Documents', options.logFilename);
if (fs.existsSync(logFile)) {
emitter.emit('log-debug', __('Removing old log file: %s', logFile));
fs.unlinkSync(logFile);
return true;
}
if (fs.existsSync(dir)) {
return fs.readdirSync(dir).some(function (name) {
var subdir = path.join(dir, name);
if (fs.existsSync(subdir) && fs.statSync(subdir).isDirectory()) {
return walk(subdir);
}
});
}
}(simHandle.dataDir));
}
var cleanupOnce = false;
function cleanupAndEmit(evt) {
if (!cleanupOnce) {
cleanupOnce = true;
}
simHandle.systemLogTail && simHandle.systemLogTail.unwatch();
simHandle.systemLogTail = null;
if (watchSimHandle) {
watchSimHandle.systemLogTail && watchSimHandle.systemLogTail.unwatch();
watchSimHandle.systemLogTail = null;
}
emitter.emit.apply(emitter, arguments);
}
function getCrashes() {
if (crashFileRegExp && fs.existsSync(detectedSimInfo.simulators.crashDir)) {
return fs.readdirSync(detectedSimInfo.simulators.crashDir).filter(function (n) { return crashFileRegExp.test(n); });
}
return [];
}
function checkIfCrashed() {
var crashes = getCrashes(),
diffCrashes = crashes
.filter(function (file) {
return existingCrashes.indexOf(file) === -1;
})
.map(function (file) {
return path.join(detectedSimInfo.simulators.crashDir, file);
})
.sort();
if (diffCrashes.length) {
// when a crash occurs, we need to provide the plist crash information as a result object
diffCrashes.forEach(function (crashFile) {
emitter.emit('log-debug', __('Detected crash file: %s', crashFile));
});
cleanupAndEmit('app-quit', new SimulatorCrash(diffCrashes));
return true;
}
return false;
}
function startSimulator(handle) {
var booted = false,
simEmitter = new EventEmitter;
function simExited(code, signal) {
if (code || code === 0) {
emitter.emit('log-debug', __('%s Simulator has exited with code %s', handle.name, code));
} else {
emitter.emit('log-debug', __('%s Simulator has exited', handle.name));
}
handle.systemLogTail && handle.systemLogTail.unwatch();
handle.systemLogTail = null;
simEmitter.emit('stop', code);
}
async.series([
function checkIfRunningAndBooted(next) {
emitter.emit('log-debug', __('Checking if simulator %s is already running', handle.simulator));
isRunning(handle.simulator, function (err, pid, udid) {
if (err) {
emitter.emit('log-debug', __('Failed to check if simulator is running: %s', err.message || err.toString()));
return next(err);
}
if (!pid) {
emitter.emit('log-debug', __('Simulator is not running'));
return next();
}
emitter.emit('log-debug', __('Simulator is running (pid %s)', pid));
// check the udid
if (udid !== handle.udid) {
emitter.emit('log-debug', __('%s Simulator is running, but not the udid we want, stopping simulator', handle.name));
stop(handle, next);
return;
}
simctl.getSim({
simctl: handle.simctl,
udid: handle.udid
}, function (err, sim) {
if (err) {
return next(err);
}
if (!sim) {
// this should never happen
return next(new Error(__('Unable to find simulator %s', handle.udid)));
}
if (sim.availability !== '(available)') {
// this should never happen
return next(new Error(__('Simulator is not available')));
}
if (/^shutdown/i.test(sim.state)) {
// the udid that is supposed to be running isn't, kill the simulator
emitter.emit('log-debug', __('%s Simulator is running, but udid %s is shut down, stopping simulator', handle.name, handle.udid));
stop(handle, next);
return;
}
simctl.waitUntilBooted({ simctl: handle.simctl, udid: handle.udid, timeout: 30000 }, function (err, _booted) {
if (err && err.code !== 666) {
emitter.emit('log-debug', __('Error while waiting for simulator to boot: %s', err.message || err.toString()));
return next(err);
}