Class: RunLoop::SimControl

Inherits:
Object
  • Object
show all
Defined in:
lib/run_loop/sim_control.rb

Overview

TODO:

‘puts` calls need to be replaced with proper logging

Note:

All command line tools are run in the context of ‘xcrun`.

One class interact with the iOS Simulators.

Throughout this class’s documentation, there are references to the _current version of Xcode_. The current Xcode version is the one returned by ‘xcrun xcodebuild`. The current Xcode version can be set using `xcode-select` or overridden using the `DEVELOPER_DIR`.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.terminate_all_simsObject

Note:

Sends ‘kill -9` to all Simulator processes. Use sparingly or not at all.

Terminates all simulators.

SimulatorBridge launchd_sim ScriptAgent

There can be only one simulator running at a time. However, during gem testing, situations can arise where multiple simulators are active.



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
# File 'lib/run_loop/sim_control.rb', line 128

def self.terminate_all_sims

  # @todo Throwing SpringBoard crashed UI dialog.
  # Tried the gentle approach first; it did not work.
  # SimControl.new.quit_sim({:post_quit_wait => 0.5})

  processes =
        ['iPhone Simulator.app', 'iOS Simulator.app',

         # Multiple launchd_sim processes have been causing problems.  This
         # is a first pass at investigating what it would mean to kill the
         # launchd_sim process.
         'launchd_sim'

        # RE: Throwing SpringBoard crashed UI dialog
        # These are children of launchd_sim.  I tried quiting them
        # to suppress related UI dialogs about crashing processes.  Killing
        # them can throw 'launchd_sim' UI Dialogs
        #'SimulatorBridge', 'SpringBoard', 'ScriptAgent', 'configd_sim', 'xpcproxy_sim'
        ]

  # @todo Maybe should try to send -TERM first and -KILL if TERM fails.
  # @todo Needs benchmarking.
  processes.each do |process_name|
    descripts = `xcrun ps x -o pid,command | grep "#{process_name}" | grep -v grep`.strip.split("\n")
    descripts.each do |process_desc|
      pid = process_desc.split(' ').first
      Open3.popen3("xcrun kill -9 #{pid} && xcrun wait #{pid}") do  |_, stdout,  stderr, _|
        if ENV['DEBUG_UNIX_CALLS'] == '1'
          out = stdout.read.strip
          err = stderr.read.strip
          next if out.to_s.empty? and err.to_s.empty?
          puts "Terminate all simulators: kill process '#{process_name}: #{pid}' => stdout: '#{out}' | stderr: '#{err}'"
        end
      end
    end
  end
end

Instance Method Details

#accessibility_enabled?(device) ⇒ Boolean

Returns:

  • (Boolean)


336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/run_loop/sim_control.rb', line 336

def accessibility_enabled?(device)
  plist = device.simulator_accessibility_plist_path
  return false unless File.exist?(plist)

  if device.version >= RunLoop::Version.new('8.0')
    plist_hash = SDK_80_ACCESSIBILITY_PROPERTIES_HASH
  else
    plist_hash = SDK_LT_80_ACCESSIBILITY_PROPERTIES_HASH
  end

  plist_hash.each do |_, details|
    key = details[:key]
    value = details[:value]

    unless pbuddy.plist_read(key, plist) == "#{value}"
      return false
    end
  end
  true
end

#enable_accessibility(device) ⇒ Object



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
# File 'lib/run_loop/sim_control.rb', line 365

def enable_accessibility(device)
  debug_logging = RunLoop::Environment.debug?

  quit_sim

  plist_path = device.simulator_accessibility_plist_path

  if device.version >= RunLoop::Version.new('8.0')
    plist_hash = SDK_80_ACCESSIBILITY_PROPERTIES_HASH
  else
    plist_hash = SDK_LT_80_ACCESSIBILITY_PROPERTIES_HASH
  end

  unless File.exist? plist_path
    preferences_dir = File.join(device.simulator_root_dir, 'data/Library/Preferences')
    FileUtils.mkdir_p(preferences_dir)
    plist = CFPropertyList::List.new
    data = {}
    plist.value = CFPropertyList.guess(data)
    plist.save(plist_path, CFPropertyList::List::FORMAT_BINARY)
  end

  msgs = []

  successes = plist_hash.map do |hash_key, settings|
    success = pbuddy.plist_set(settings[:key], settings[:type], settings[:value], plist_path)
    unless success
      if debug_logging
        if settings[:type] == 'bool'
          value = settings[:value] ? 'YES' : 'NO'
        else
          value = settings[:value]
        end
        msgs << "could not set #{hash_key} => '#{settings[:key]}' to #{value}"
      end
    end
    success
  end

  if successes.all?
    true
  else
    return false, msgs
  end
end

#enable_software_keyboard(device) ⇒ Object



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
# File 'lib/run_loop/sim_control.rb', line 438

def enable_software_keyboard(device)
  debug_logging = RunLoop::Environment.debug?

  quit_sim

  plist_path = device.simulator_preferences_plist_path

  unless File.exist? plist_path
    preferences_dir = File.join(device.simulator_root_dir, 'data/Library/Preferences')
    FileUtils.mkdir_p(preferences_dir)
    plist = CFPropertyList::List.new
    data = {}
    plist.value = CFPropertyList.guess(data)
    plist.save(plist_path, CFPropertyList::List::FORMAT_BINARY)
  end

  msgs = []

  successes = CORE_SIMULATOR_KEYBOARD_PROPERTIES_HASH.map do |hash_key, settings|
    success = pbuddy.plist_set(settings[:key], settings[:type], settings[:value], plist_path)
    unless success
      if debug_logging
        if settings[:type] == 'bool'
          value = settings[:value] ? 'YES' : 'NO'
        else
          value = settings[:value]
        end
        msgs << "could not set #{hash_key} => '#{settings[:key]}' to #{value}"
      end
    end
    success
  end

  if successes.all?
    true
  else
    return false, msgs
  end
end

#ensure_accessibility(device) ⇒ Object



357
358
359
360
361
362
363
# File 'lib/run_loop/sim_control.rb', line 357

def ensure_accessibility(device)
  if accessibility_enabled?(device)
    true
  else
    enable_accessibility(device)
  end
end

#ensure_software_keyboard(device) ⇒ Object



430
431
432
433
434
435
436
# File 'lib/run_loop/sim_control.rb', line 430

def ensure_software_keyboard(device)
  if software_keyboard_enabled?(device)
    true
  else
    enable_software_keyboard(device)
  end
end

#launch_sim(opts = {}) ⇒ Object

TODO:

Consider migrating apple script call to xctools.

If it is not already running, launch the simulator for the current version of Xcode.

Parameters:

  • opts (Hash) (defaults to: {})

    Optional controls.

Options Hash (opts):

  • :post_launch_wait (Float) — default: 2.0

    How long to sleep after the simulator has launched.

  • :hide_after (Boolean) — default: false

    If true, will attempt to Hide the simulator after it is launched. This is useful ‘only when testing gem features` that require the simulator be launched repeated and you are tired of your editor losing focus. :)



83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/run_loop/sim_control.rb', line 83

def launch_sim(opts={})
  unless sim_is_running?
    default_opts = {:post_launch_wait => RunLoop::Environment.sim_post_launch_wait || 2.0,
                    :hide_after => false}
    merged_opts = default_opts.merge(opts)
    `xcrun open -a "#{sim_app_path}"`
    if merged_opts[:hide_after]
      `xcrun /usr/bin/osascript -e 'tell application "System Events" to keystroke "h" using command down'`
    end
    sleep(merged_opts[:post_launch_wait]) if merged_opts[:post_launch_wait]
  end
end

#pbuddyRunLoop::PlistBuddy

Return an instance of PlistBuddy.

Returns:



45
46
47
# File 'lib/run_loop/sim_control.rb', line 45

def pbuddy
  @pbuddy ||= RunLoop::PlistBuddy.new
end

#quit_sim(opts = {}) ⇒ Object

TODO:

Consider migrating apple script call to xctools.

If it is running, quit the simulator for the current version of Xcode.

Parameters:

  • opts (Hash) (defaults to: {})

    Optional controls.

Options Hash (opts):

  • :post_quit_wait (Float) — default: 1.0

    How long to sleep after the simulator has quit.



62
63
64
65
66
67
68
69
# File 'lib/run_loop/sim_control.rb', line 62

def quit_sim(opts={})
  if sim_is_running?
    default_opts = {:post_quit_wait => 1.0 }
    merged_opts = default_opts.merge(opts)
    `echo 'application "#{sim_name}" quit' | xcrun osascript`
    sleep(merged_opts[:post_quit_wait]) if merged_opts[:post_quit_wait]
  end
end

#relaunch_sim(opts = {}) ⇒ Object

Relaunch the simulator for the current version of Xcode. If that simulator is already running, it is quit.

Parameters:

  • opts (Hash) (defaults to: {})

    Optional controls.

Options Hash (opts):

  • :post_quit_wait (Float) — default: 1.0

    How long to sleep after the simulator has quit.

  • :post_launch_wait (Float) — default: 2.0

    How long to sleep after the simulator has launched.

  • :hide_after (Boolean) — default: false

    If true, will attempt to Hide the simulator after it is launched. This is useful ‘only when testing gem features` that require the simulator be launched repeated and you are tired of your editor losing focus. :)



108
109
110
111
112
113
114
115
# File 'lib/run_loop/sim_control.rb', line 108

def relaunch_sim(opts={})
  default_opts = {:post_quit_wait => 1.0,
                  :post_launch_wait => RunLoop::Environment.sim_post_launch_wait || 2.0,
                  :hide_after => false}
  merged_opts = default_opts.merge(opts)
  quit_sim(merged_opts)
  launch_sim(merged_opts)
end

#reset_sim_content_and_settings(opts = {}) ⇒ Object

Resets the simulator content and settings.

In Xcode < 6, it is analogous to touching the menu item _for every

simulator_, regardless of SDK.

In Xcode 6, the default is the same; the content and settings for every

simulator is erased.  However, in Xcode 6 it is possible to pass
a `:sim_udid` as a option to erase an individual simulator.

On Xcode 5, it works by deleting the following directories:

  • ~/Library/Application Support/iPhone Simulator/Library

  • ~/Library/Application Support/iPhone Simulator/Library/<sdk>

and relaunching the iOS Simulator which will recreate the Library directory and the latest SDK directory.

On Xcode 6, it uses the ‘simctl erase <udid>` command line tool.

Parameters:

  • opts (Hash) (defaults to: {})

    Optional controls for quitting and launching the simulator.

Options Hash (opts):

  • :post_quit_wait (Float) — default: 1.0

    How long to sleep after the simulator has quit.

  • :post_launch_wait (Float) — default: 3.0

    How long to sleep after the simulator has launched. Waits longer than normal because we need the simulator directories to be repopulated. NOTE: This option is ignored in Xcode 6.

  • :hide_after (Boolean) — default: false

    If true, will attempt to Hide the simulator after it is launched. This is useful ‘only when testing gem features` that require the simulator be launched repeated and you are tired of your editor losing focus. :) NOTE: This option is ignored in Xcode 6.

  • :sim_udid (String) — default: nil

    The udid of the simulator to reset. NOTE: This option is ignored in Xcode < 6.



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
# File 'lib/run_loop/sim_control.rb', line 200

def reset_sim_content_and_settings(opts={})
  default_opts = {:post_quit_wait => 1.0,
                  :post_launch_wait => RunLoop::Environment.sim_post_launch_wait || 3.0,
                  :hide_after => false,
                  :sim_udid => nil}
  merged_opts = default_opts.merge(opts)

  quit_sim(merged_opts)

  # WARNING - DO NOT TRY TO DELETE Developer/CoreSimulator/Devices!
  # Very bad things will happen.  Unlike Xcode < 6, the re-launching the
  # simulator will _not_ recreate the SDK (aka Devices) directories.
  if xcode_version_gte_6?
    simctl_reset(merged_opts[:sim_udid])
  else
    sim_lib_path = File.join(sim_app_support_dir, 'Library')
    FileUtils.rm_rf(sim_lib_path)
    existing_sim_sdk_or_device_data_dirs.each do |dir|
      FileUtils.rm_rf(dir)
    end
    launch_sim(merged_opts)

    # This is tricky because we need to wait for the simulator to recreate
    # the directories.  Specifically, we need the Accessibility plist to be
    # exist so subsequent calabash launches will be able to enable
    # accessibility.
    #
    # The directories take ~3.0 - ~5.0 to create.
    counter = 0
    loop do
      break if counter == 80
      dirs = existing_sim_sdk_or_device_data_dirs
      if dirs.count == 0
        sleep(0.2)
      else
        break if dirs.all? { |dir|
          plist = File.expand_path("#{dir}/Library/Preferences/com.apple.Accessibility.plist")
          File.exist?(plist)
        }
        sleep(0.2)
      end
      counter = counter + 1
    end
  end
end

#sim_is_running?Boolean

Is the simulator for the current version of Xcode running?

Returns:

  • (Boolean)

    True if the simulator is running.



51
52
53
# File 'lib/run_loop/sim_control.rb', line 51

def sim_is_running?
  not sim_pid.nil?
end

#sim_udid?(udid) ⇒ Boolean

Is the arg a valid Xcode >= 6.0 simulator udid?

Parameters:

  • udid (String)

    the String to check

Returns:



313
314
315
# File 'lib/run_loop/sim_control.rb', line 313

def sim_udid?(udid)
  udid.length == 36 and udid[XCODE_6_SIM_UDID_REGEX,0] != nil
end

#simulatorsObject



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/run_loop/sim_control.rb', line 317

def simulators
  unless xcode_version_gte_51?
    raise RuntimeError, 'simctl is only available on Xcode >= 6'
  end

  if xcode_version_gte_6?
    hash = simctl_list :devices
    sims = []
    hash.each_pair do |sdk, list|
      list.each do |details|
        sims << RunLoop::Device.new(details[:name], sdk, details[:udid], details[:state])
      end
    end
    sims
  else
    raise NotImplementedError, 'the simulators method is not available yet for Xcode 5.1.1'
  end
end

#software_keyboard_enabled?(device) ⇒ Boolean

Returns:

  • (Boolean)


411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/run_loop/sim_control.rb', line 411

def software_keyboard_enabled?(device)
  unless xcode_version_gte_51?
    raise RuntimeError, 'Keyboard enabling is only available on Xcode >= 6'
  end

  plist = device.simulator_preferences_plist_path
  return false unless File.exist?(plist)

  CORE_SIMULATOR_KEYBOARD_PROPERTIES_HASH.each do |_, details|
    key = details[:key]
    value = details[:value]

    unless pbuddy.plist_read(key, plist) == "#{value}"
      return false
    end
  end
  true
end

#xctoolsRunLoop::XCTools

Returns an instance of XCTools.

Returns:



19
20
21
# File 'lib/run_loop/sim_control.rb', line 19

def xctools
  @xctools ||= RunLoop::XCTools.new
end