run_loop 1.5.1 → 1.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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