run_loop 1.5.1 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/run_loop.rb +2 -0
- data/lib/run_loop/app.rb +5 -0
- data/lib/run_loop/cli/instruments.rb +0 -1
- data/lib/run_loop/cli/simctl.rb +98 -13
- data/lib/run_loop/core.rb +194 -110
- data/lib/run_loop/device.rb +218 -2
- data/lib/run_loop/directory.rb +22 -5
- data/lib/run_loop/environment.rb +10 -0
- data/lib/run_loop/instruments.rb +55 -72
- data/lib/run_loop/life_cycle/core_simulator.rb +544 -0
- data/lib/run_loop/logging.rb +68 -0
- data/lib/run_loop/simctl/bridge.rb +15 -2
- data/lib/run_loop/version.rb +1 -1
- data/lib/run_loop/xcode.rb +15 -0
- data/lib/run_loop/xcrun.rb +75 -0
- data/lib/run_loop/xctools.rb +19 -0
- metadata +5 -16
@@ -0,0 +1,544 @@
|
|
1
|
+
module RunLoop
|
2
|
+
module LifeCycle
|
3
|
+
|
4
|
+
class CoreSimulator
|
5
|
+
|
6
|
+
require 'securerandom'
|
7
|
+
|
8
|
+
# @!visibility private
|
9
|
+
METADATA_PLIST = '.com.apple.mobile_container_manager.metadata.plist'
|
10
|
+
|
11
|
+
# @!visibility private
|
12
|
+
CORE_SIMULATOR_DEVICE_DIR = File.expand_path('~/Library/Developer/CoreSimulator/Devices')
|
13
|
+
|
14
|
+
# @!visibility private
|
15
|
+
# Pattern.
|
16
|
+
# [ '< process name >', < send term first > ]
|
17
|
+
MANAGED_PROCESSES =
|
18
|
+
[
|
19
|
+
# This process is a daemon, and requires 'KILL' to terminate.
|
20
|
+
# Killing the process is fast, but it takes a long time to
|
21
|
+
# restart.
|
22
|
+
# ['com.apple.CoreSimulator.CoreSimulatorService', false],
|
23
|
+
|
24
|
+
# Probably do not need to quit this, but it is tempting to do so.
|
25
|
+
#['com.apple.CoreSimulator.SimVerificationService', false],
|
26
|
+
|
27
|
+
# Started by Xamarin Studio, this is the parent process of the
|
28
|
+
# processes launched by Xamarin's interaction with
|
29
|
+
# CoreSimulatorBridge.
|
30
|
+
['csproxy', true],
|
31
|
+
|
32
|
+
# Yes.
|
33
|
+
['SimulatorBridge', true],
|
34
|
+
['configd_sim', true],
|
35
|
+
['launchd_sim', true],
|
36
|
+
|
37
|
+
# Does not always appear.
|
38
|
+
['CoreSimulatorBridge', true],
|
39
|
+
|
40
|
+
# Xcode 7
|
41
|
+
['ids_simd', true]
|
42
|
+
]
|
43
|
+
|
44
|
+
# @!visibility private
|
45
|
+
# How long to wait after the simulator has launched.
|
46
|
+
SIM_POST_LAUNCH_WAIT = RunLoop::Environment.sim_post_launch_wait || 1.0
|
47
|
+
|
48
|
+
# @!visibility private
|
49
|
+
# How long to wait for for a device to reach a state.
|
50
|
+
WAIT_FOR_DEVICE_STATE_OPTS =
|
51
|
+
{
|
52
|
+
interval: 0.1,
|
53
|
+
timeout: 5
|
54
|
+
}
|
55
|
+
|
56
|
+
# @!visibility private
|
57
|
+
# How long to wait for the CoreSimulator processes to start.
|
58
|
+
WAIT_FOR_SIMULATOR_PROCESSES_OPTS =
|
59
|
+
{
|
60
|
+
timeout: 5,
|
61
|
+
raise_on_timeout: true
|
62
|
+
}
|
63
|
+
|
64
|
+
attr_reader :app
|
65
|
+
attr_reader :device
|
66
|
+
attr_reader :sim_control
|
67
|
+
attr_reader :pbuddy
|
68
|
+
|
69
|
+
# @param [RunLoop::App] app The application.
|
70
|
+
# @param [RunLoop::Device] device The device.
|
71
|
+
def initialize(app, device, sim_control=RunLoop::SimControl.new)
|
72
|
+
@app = app
|
73
|
+
@device = device
|
74
|
+
@sim_control = sim_control
|
75
|
+
|
76
|
+
# In order to manage the app on the device, we need to manage the
|
77
|
+
# CoreSimulator processes.
|
78
|
+
RunLoop::SimControl.terminate_all_sims
|
79
|
+
terminate_core_simulator_processes
|
80
|
+
end
|
81
|
+
|
82
|
+
# Launch simulator without specifying an app.
|
83
|
+
def launch_simulator
|
84
|
+
sim_path = sim_control.send(:sim_app_path)
|
85
|
+
args = ['open', '-g', '-a', sim_path, '--args', '-CurrentDeviceUDID', device.udid]
|
86
|
+
|
87
|
+
RunLoop.log_debug("Launching #{device} with:")
|
88
|
+
RunLoop.log_unix_cmd("xcrun #{args.join(' ')}")
|
89
|
+
|
90
|
+
start_time = Time.now
|
91
|
+
|
92
|
+
pid = spawn('xcrun', *args)
|
93
|
+
Process.detach(pid)
|
94
|
+
|
95
|
+
sim_name = sim_control.send(:sim_name)
|
96
|
+
|
97
|
+
RunLoop::ProcessWaiter.new(sim_name, WAIT_FOR_SIMULATOR_PROCESSES_OPTS).wait_for_any
|
98
|
+
|
99
|
+
device.simulator_wait_for_stable_state
|
100
|
+
|
101
|
+
elapsed = Time.now - start_time
|
102
|
+
RunLoop.log_debug("Took #{elapsed} seconds to launch the simulator")
|
103
|
+
|
104
|
+
true
|
105
|
+
end
|
106
|
+
|
107
|
+
# @!visibility private
|
108
|
+
def pbuddy
|
109
|
+
@pbuddy ||= RunLoop::PlistBuddy.new
|
110
|
+
end
|
111
|
+
|
112
|
+
# @!visibility private
|
113
|
+
def sdk_gte_8?
|
114
|
+
device.version >= RunLoop::Version.new('8.0')
|
115
|
+
end
|
116
|
+
|
117
|
+
# The data directory for the the device.
|
118
|
+
#
|
119
|
+
# ~/Library/Developer/CoreSimulator/Devices/<UDID>/data
|
120
|
+
def device_data_dir
|
121
|
+
@device_data_dir ||= File.join(CORE_SIMULATOR_DEVICE_DIR, device.udid, 'data')
|
122
|
+
end
|
123
|
+
|
124
|
+
# The applications directory for the device.
|
125
|
+
#
|
126
|
+
# ~/Library/Developer/CoreSimulator/Devices/<UDID>/Containers/Bundle/Application
|
127
|
+
def device_applications_dir
|
128
|
+
@device_app_dir ||= lambda do
|
129
|
+
if sdk_gte_8?
|
130
|
+
File.join(device_data_dir, 'Containers', 'Bundle', 'Application')
|
131
|
+
else
|
132
|
+
File.join(device_data_dir, 'Applications')
|
133
|
+
end
|
134
|
+
end.call
|
135
|
+
end
|
136
|
+
|
137
|
+
# The sandbox directory for the app.
|
138
|
+
#
|
139
|
+
# ~/Library/Developer/CoreSimulator/Devices/<UDID>/Containers/Data/Application
|
140
|
+
#
|
141
|
+
# Contains Library, Documents, and tmp directories.
|
142
|
+
def app_sandbox_dir
|
143
|
+
app_install_dir = installed_app_bundle_dir
|
144
|
+
return nil if app_install_dir.nil?
|
145
|
+
if sdk_gte_8?
|
146
|
+
app_sandbox_dir_sdk_gte_8
|
147
|
+
else
|
148
|
+
app_install_dir
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# The Library directory in the sandbox.
|
153
|
+
def app_library_dir
|
154
|
+
base_dir = app_sandbox_dir
|
155
|
+
if base_dir.nil?
|
156
|
+
nil
|
157
|
+
else
|
158
|
+
File.join(base_dir, 'Library')
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# The Library/Preferences directory in the sandbox.
|
163
|
+
def app_library_preferences_dir
|
164
|
+
base_dir = app_library_dir
|
165
|
+
if base_dir.nil?
|
166
|
+
nil
|
167
|
+
else
|
168
|
+
File.join(base_dir, 'Preferences')
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# The Documents directory in the sandbox.
|
173
|
+
def app_documents_dir
|
174
|
+
base_dir = app_sandbox_dir
|
175
|
+
if base_dir.nil?
|
176
|
+
nil
|
177
|
+
else
|
178
|
+
File.join(base_dir, 'Documents')
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# The tmp directory in the sandbox.
|
183
|
+
def app_tmp_dir
|
184
|
+
base_dir = app_sandbox_dir
|
185
|
+
if base_dir.nil?
|
186
|
+
nil
|
187
|
+
else
|
188
|
+
File.join(base_dir, 'tmp')
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Is this app installed?
|
193
|
+
def app_is_installed?
|
194
|
+
!installed_app_bundle_dir.nil?
|
195
|
+
end
|
196
|
+
|
197
|
+
# The sha1 of the installed app.
|
198
|
+
def installed_app_sha1
|
199
|
+
installed_bundle = installed_app_bundle_dir
|
200
|
+
if installed_bundle
|
201
|
+
RunLoop::Directory.directory_digest(installed_bundle)
|
202
|
+
else
|
203
|
+
nil
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Is the app that is install the same as the one we have in hand?
|
208
|
+
def same_sha1_as_installed?
|
209
|
+
app.sha1 == installed_app_sha1
|
210
|
+
end
|
211
|
+
|
212
|
+
# @!visibility private
|
213
|
+
#
|
214
|
+
# Returns the path to the installed app bundle directory (.app).
|
215
|
+
#
|
216
|
+
# If this method returns nil, the app is not installed.
|
217
|
+
def installed_app_bundle_dir
|
218
|
+
sim_app_dir = device_applications_dir
|
219
|
+
return nil if !File.exist?(sim_app_dir)
|
220
|
+
Dir.glob("#{sim_app_dir}/**/*.app").find do |path|
|
221
|
+
RunLoop::App.new(path).bundle_identifier == app.bundle_identifier
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# Uninstall the app on the device.
|
226
|
+
def uninstall
|
227
|
+
installed_app_bundle = installed_app_bundle_dir
|
228
|
+
if installed_app_bundle
|
229
|
+
uninstall_app_and_sandbox(installed_app_bundle)
|
230
|
+
:uninstalled
|
231
|
+
else
|
232
|
+
RunLoop.log_debug('App was not installed. Nothing to do')
|
233
|
+
:not_installed
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Install the app on the device.
|
238
|
+
def install
|
239
|
+
installed_app_bundle = installed_app_bundle_dir
|
240
|
+
|
241
|
+
# App is not installed.
|
242
|
+
return install_new_app if installed_app_bundle.nil?
|
243
|
+
|
244
|
+
# App is installed but sha1 is different.
|
245
|
+
if !same_sha1_as_installed?
|
246
|
+
return reinstall_existing_app_and_clear_sandbox(installed_app_bundle)
|
247
|
+
end
|
248
|
+
|
249
|
+
RunLoop.log_debug('The installed app is the same as the app we are trying to install; skipping installation')
|
250
|
+
installed_app_bundle
|
251
|
+
end
|
252
|
+
|
253
|
+
# @!visibility private
|
254
|
+
#
|
255
|
+
# 1. Does nothing if the app is not installed.
|
256
|
+
# 2. Does nothing if the app the same as the app that is installed
|
257
|
+
# 3. Installs app if it is different from the installed app
|
258
|
+
#
|
259
|
+
# TODO needs unit tests and a better name?
|
260
|
+
def ensure_app_same
|
261
|
+
installed_app_bundle = installed_app_bundle_dir
|
262
|
+
|
263
|
+
if !installed_app_bundle
|
264
|
+
RunLoop.log_debug("App: #{app} is not installed")
|
265
|
+
return true
|
266
|
+
end
|
267
|
+
|
268
|
+
installed_sha = installed_app_sha1
|
269
|
+
app_sha = app.sha1
|
270
|
+
|
271
|
+
if installed_sha == app_sha
|
272
|
+
RunLoop.log_debug("Installed app is the same as #{app}")
|
273
|
+
return true
|
274
|
+
end
|
275
|
+
|
276
|
+
RunLoop.log_debug("The app you are trying to launch is not the same as the app that is installed.")
|
277
|
+
RunLoop.log_debug(" Installed app SHA: #{installed_sha}")
|
278
|
+
RunLoop.log_debug(" App to launch SHA: #{app_sha}")
|
279
|
+
RunLoop.log_debug("Will install #{app}")
|
280
|
+
|
281
|
+
|
282
|
+
FileUtils.rm_rf installed_app_bundle
|
283
|
+
RunLoop.log_debug('Deleted the existing app')
|
284
|
+
|
285
|
+
directory = File.expand_path(File.join(installed_app_bundle, '..'))
|
286
|
+
bundle_name = File.basename(app.path)
|
287
|
+
target = File.join(directory, bundle_name)
|
288
|
+
|
289
|
+
args = ['ditto', app.path, target]
|
290
|
+
RunLoop::Xcrun.new.exec(args, log_cmd: true)
|
291
|
+
|
292
|
+
RunLoop.log_debug("Installed #{app} on CoreSimulator #{device.udid}")
|
293
|
+
|
294
|
+
true
|
295
|
+
end
|
296
|
+
|
297
|
+
# Reset app sandbox.
|
298
|
+
def reset_app_sandbox
|
299
|
+
return true if !app_is_installed?
|
300
|
+
|
301
|
+
wait_for_device_state('Shutdown')
|
302
|
+
|
303
|
+
reset_app_sandbox_internal
|
304
|
+
end
|
305
|
+
|
306
|
+
private
|
307
|
+
|
308
|
+
def generate_uuid
|
309
|
+
SecureRandom.uuid.upcase!
|
310
|
+
end
|
311
|
+
|
312
|
+
def existing_app_container_uuids
|
313
|
+
if File.exist?(device_applications_dir)
|
314
|
+
Dir.entries(device_applications_dir)
|
315
|
+
else
|
316
|
+
[]
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def generate_unique_uuid(existing, timeout=1.0)
|
321
|
+
begin
|
322
|
+
Timeout::timeout(timeout, Timeout::Error) do
|
323
|
+
uuid = generate_uuid
|
324
|
+
loop do
|
325
|
+
break if !existing.include?(uuid)
|
326
|
+
uuid = generate_uuid
|
327
|
+
end
|
328
|
+
uuid
|
329
|
+
end
|
330
|
+
rescue Timeout::Error => _
|
331
|
+
raise RuntimeError,
|
332
|
+
"Expected to be able to generate a unique uuid in #{timeout} seconds"
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
def install_new_app
|
337
|
+
wait_for_device_state('Shutdown')
|
338
|
+
|
339
|
+
existing = existing_app_container_uuids
|
340
|
+
udid = generate_unique_uuid(existing)
|
341
|
+
directory = File.join(device_applications_dir, udid)
|
342
|
+
|
343
|
+
bundle_name = File.basename(app.path)
|
344
|
+
target = File.join(directory, bundle_name)
|
345
|
+
|
346
|
+
args = ['ditto', app.path, target]
|
347
|
+
RunLoop::Xcrun.new.exec(args, log_cmd: true)
|
348
|
+
target
|
349
|
+
end
|
350
|
+
|
351
|
+
def reinstall_existing_app_and_clear_sandbox(installed_app_bundle)
|
352
|
+
wait_for_device_state('Shutdown')
|
353
|
+
|
354
|
+
reset_app_sandbox_internal
|
355
|
+
|
356
|
+
if File.exist?(installed_app_bundle)
|
357
|
+
FileUtils.rm_rf(installed_app_bundle)
|
358
|
+
RunLoop.log_debug("Deleted app bundle: #{installed_app_bundle}")
|
359
|
+
end
|
360
|
+
|
361
|
+
directory = File.dirname(installed_app_bundle)
|
362
|
+
bundle_name = File.basename(app.path)
|
363
|
+
target = File.join(directory, bundle_name)
|
364
|
+
|
365
|
+
args = ['ditto', app.path, target]
|
366
|
+
RunLoop::Xcrun.new.exec(args, log_cmd: true)
|
367
|
+
installed_app_bundle
|
368
|
+
end
|
369
|
+
|
370
|
+
def uninstall_app_and_sandbox(installed_app_bundle)
|
371
|
+
wait_for_device_state('Shutdown')
|
372
|
+
|
373
|
+
if sdk_gte_8?
|
374
|
+
# Must delete the sandbox first.
|
375
|
+
directory = app_sandbox_dir
|
376
|
+
if File.exist?(directory)
|
377
|
+
FileUtils.rm_rf(directory)
|
378
|
+
RunLoop.log_debug("Deleted app sandbox: #{directory}")
|
379
|
+
end
|
380
|
+
|
381
|
+
directory = File.dirname(installed_app_bundle)
|
382
|
+
if File.exist?(directory)
|
383
|
+
FileUtils.rm_rf(directory)
|
384
|
+
RunLoop.log_debug("Deleted app container: #{directory}")
|
385
|
+
end
|
386
|
+
else
|
387
|
+
# Sandbox _is_ in the container.
|
388
|
+
directory = File.dirname(installed_app_bundle)
|
389
|
+
if File.exist?(directory)
|
390
|
+
FileUtils.rm_rf(directory)
|
391
|
+
RunLoop.log_debug("Deleted app container: #{directory}")
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
# @!visibility private
|
397
|
+
def app_sandbox_dir_sdk_gte_8
|
398
|
+
containers_data_dir = File.join(device_data_dir, 'Containers', 'Data', 'Application')
|
399
|
+
apps = Dir.glob("#{containers_data_dir}/**/#{METADATA_PLIST}")
|
400
|
+
match = apps.find do |metadata_plist|
|
401
|
+
pbuddy.plist_read('MCMMetadataIdentifier', metadata_plist) == app.bundle_identifier
|
402
|
+
end
|
403
|
+
if match
|
404
|
+
File.dirname(match)
|
405
|
+
else
|
406
|
+
nil
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
# @!visibility private
|
411
|
+
def terminate_core_simulator_processes
|
412
|
+
MANAGED_PROCESSES.each do |pair|
|
413
|
+
name = pair[0]
|
414
|
+
send_term = pair[1]
|
415
|
+
pids = RunLoop::ProcessWaiter.new(name).pids
|
416
|
+
pids.each do |pid|
|
417
|
+
|
418
|
+
if send_term
|
419
|
+
term = RunLoop::ProcessTerminator.new(pid, 'TERM', name)
|
420
|
+
killed = term.kill_process
|
421
|
+
else
|
422
|
+
killed = false
|
423
|
+
end
|
424
|
+
|
425
|
+
unless killed
|
426
|
+
term = RunLoop::ProcessTerminator.new(pid, 'KILL', name)
|
427
|
+
term.kill_process
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
# @!visibility private
|
434
|
+
def wait_for_device_state(target_state)
|
435
|
+
now = Time.now
|
436
|
+
timeout = WAIT_FOR_DEVICE_STATE_OPTS[:timeout]
|
437
|
+
poll_until = now + timeout
|
438
|
+
delay = WAIT_FOR_DEVICE_STATE_OPTS[:interval]
|
439
|
+
in_state = false
|
440
|
+
while Time.now < poll_until
|
441
|
+
in_state = device.update_simulator_state == target_state
|
442
|
+
break if in_state
|
443
|
+
sleep delay
|
444
|
+
end
|
445
|
+
|
446
|
+
elapsed = Time.now - now
|
447
|
+
RunLoop.log_debug("Waited for #{elapsed} seconds for device to have state: '#{target_state}'.")
|
448
|
+
|
449
|
+
unless in_state
|
450
|
+
raise "Expected '#{target_state} but found '#{device.state}' after waiting."
|
451
|
+
end
|
452
|
+
in_state
|
453
|
+
end
|
454
|
+
|
455
|
+
# @!visibility private
|
456
|
+
def reset_app_sandbox_internal_shared
|
457
|
+
[app_documents_dir, app_tmp_dir].each do |dir|
|
458
|
+
FileUtils.rm_rf dir
|
459
|
+
FileUtils.mkdir dir
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
# @!visibility private
|
464
|
+
def reset_app_sandbox_internal_sdk_gte_8
|
465
|
+
lib_dir = app_library_dir
|
466
|
+
RunLoop::Directory.recursive_glob_for_entries(lib_dir).each do |entry|
|
467
|
+
if entry.include?('Preferences')
|
468
|
+
# nop
|
469
|
+
else
|
470
|
+
if File.exist?(entry)
|
471
|
+
FileUtils.rm_rf(entry)
|
472
|
+
end
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
prefs_dir = app_library_preferences_dir
|
477
|
+
protected = ['com.apple.UIAutomation.plist',
|
478
|
+
'com.apple.UIAutomationPlugIn.plist']
|
479
|
+
RunLoop::Directory.recursive_glob_for_entries(prefs_dir).each do |entry|
|
480
|
+
unless protected.include?(File.basename(entry))
|
481
|
+
if File.exist?(entry)
|
482
|
+
FileUtils.rm_rf entry
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
# @!visibility private
|
489
|
+
def reset_app_sandbox_internal_sdk_lt_8
|
490
|
+
prefs_dir = app_library_preferences_dir
|
491
|
+
RunLoop::Directory.recursive_glob_for_entries(prefs_dir).each do |entry|
|
492
|
+
if entry.end_with?('.GlobalPreferences.plist') ||
|
493
|
+
entry.end_with?('com.apple.PeoplePicker.plist')
|
494
|
+
# nop
|
495
|
+
else
|
496
|
+
if File.exist?(entry)
|
497
|
+
FileUtils.rm_rf entry
|
498
|
+
end
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
# app preferences lives in device Library/Preferences
|
503
|
+
device_prefs_dir = File.join(app_sandbox_dir, 'Library', 'Preferences')
|
504
|
+
app_prefs_plist = File.join(device_prefs_dir, "#{app.bundle_identifier}.plist")
|
505
|
+
if File.exist?(app_prefs_plist)
|
506
|
+
FileUtils.rm_rf(app_prefs_plist)
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
# @!visibility private
|
511
|
+
def reset_app_sandbox_internal
|
512
|
+
reset_app_sandbox_internal_shared
|
513
|
+
|
514
|
+
if sdk_gte_8?
|
515
|
+
reset_app_sandbox_internal_sdk_gte_8
|
516
|
+
else
|
517
|
+
reset_app_sandbox_internal_sdk_lt_8
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
# @!visibility private
|
522
|
+
# For testing.
|
523
|
+
def launch
|
524
|
+
|
525
|
+
install
|
526
|
+
launch_simulator
|
527
|
+
|
528
|
+
args = ['simctl', 'launch', device.udid, app.bundle_identifier]
|
529
|
+
hash = RunLoop::Xcrun.new.exec(args, log_cmd: true, timeout: 20)
|
530
|
+
|
531
|
+
exit_status = hash[:exit_status]
|
532
|
+
|
533
|
+
if exit_status != 0
|
534
|
+
err = hash[:err]
|
535
|
+
RunLoop.log_error(err)
|
536
|
+
raise RuntimeError, "Could not launch #{app.bundle_identifier} on #{device}"
|
537
|
+
end
|
538
|
+
|
539
|
+
RunLoop::ProcessWaiter.new(app.executable_name, WAIT_FOR_APP_LAUNCH_OPTS).wait_for_any
|
540
|
+
true
|
541
|
+
end
|
542
|
+
end
|
543
|
+
end
|
544
|
+
end
|