class RunLoop::CoreSimulator

A class to manage interactions with CoreSimulators.

Constants

CORE_SIMULATOR_DEVICE_DIR

@!visibility private

DEFAULT_OPTIONS

These options control various aspects of an app’s life cycle on the iOS Simulator.

You can override these values if they do not work in your environment.

For cucumber users, the best place to override would be in your features/support/env.rb.

For example:

RunLoop::CoreSimulator::DEFAULT_OPTIONS = 60

MANAGED_PROCESSES

@!visibility private

METADATA_PLIST

@!visibility private

PREFERENCES_PLIST

@!visibility private

SIMULATOR_QUIT_PROCESSES

@!visibility private Pattern:

‘< process name >’, < send term first >
WAIT_FOR_SIMULATOR_STATE_INTERVAL

@!visibility private This should not be overridden

Attributes

app[R]

@!visibility private

device[R]

@!visibility private

pbuddy[R]

@!visibility private

sim_keyboard[R]

@!visibility private

xcode[R]

@!visibility private

xcrun[R]

@!visibility private

Public Class Methods

app_installed?(device, bundle_identifier, options={}) click to toggle source

@!visibility private

@param [RunLoop::Device, String] device a simulator UDID, instruments-ready

name, or a RunLoop::Device

@param [String] bundle_identifier the app to check for

@raise [ArgumentError] if no device can be found matching the UDID or

instruments-ready name

@raise [ArgumentError] if device is not a simulator @raise [ArgumentError] if language_code is invalid

@return [Boolean] true if the app with the identifier is installed

# File lib/run_loop/core_simulator.rb, line 364
def self.app_installed?(device, bundle_identifier, options={})
  merged_options = {:xcode => RunLoop::Xcode.new}.merge(options)

  if device.is_a?(RunLoop::Device)
    simulator = device
  else
    simulator = RunLoop::Device.device_with_identifier(device, merged_options)
  end

  if simulator.physical_device?
    raise ArgumentError, "The device must be a simulator"
  end

  start = Time.now

  installed = self.send(:user_app_installed?, device, bundle_identifier) ||
    self.send(:system_app_installed?, bundle_identifier, merged_options[:xcode])

  RunLoop.log_debug("Took #{Time.now - start} seconds to check if app was installed")
  installed
end
ensure_hardware_keyboard_connected(pbuddy) click to toggle source

@!visibility private

Connect the hardware keyboard so users can use the host machine keyboard to type text during testing.

# File lib/run_loop/core_simulator.rb, line 266
def self.ensure_hardware_keyboard_connected(pbuddy)
  plist = self.simulator_preferences_plist(pbuddy)
  pbuddy.plist_set("ConnectHardwareKeyboard", "bool", true, plist)
end
erase(simulator, options={}) click to toggle source

@!visibility private Erase a simulator. This is the same as touching the Simulator “Reset Content & Settings” menu item.

@param [RunLoop::Device] simulator The simulator to erase @param [Hash] options Control the behavior of the method. @option options [Numeric] :timeout How long to wait for simctl to

shutdown the simulator. This is necessary for the erase to succeed.

@raise RuntimeError If the simulator cannot be shutdown @raise RuntimeError If the simulator cannot be erased @raise ArgumentError If the simulator is a physical device

# File lib/run_loop/core_simulator.rb, line 283
def self.erase(simulator, options={})
  if simulator.physical_device?
    raise ArgumentError,
      "#{simulator} is a physical device.  This method is only for Simulators"
  end

  merged_options = DEFAULT_OPTIONS.merge(options)
  simctl = merged_options[:simctl] || RunLoop::Simctl.new
  timeout = merged_options[:timeout] || merged_options[:wait_for_state_timeout]

  simctl.erase(simulator,
               timeout,
               WAIT_FOR_SIMULATOR_STATE_INTERVAL)
end
hardware_keyboard_connected?(pbuddy) click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 257
def self.hardware_keyboard_connected?(pbuddy)
  plist = self.simulator_preferences_plist(pbuddy)
  pbuddy.plist_read("ConnectHardwareKeyboard", plist)
end
new(device, app, options={}) click to toggle source

@param [RunLoop::Device] device The device. @param [RunLoop::App] app The application. @param [Hash] options Controls the behavior of this class. @option options :quit_sim_on_init (true) If true, quit any running @option options :xcode An instance of Xcode to use

simulators in the initialize method.
# File lib/run_loop/core_simulator.rb, line 392
def initialize(device, app, options={})
  @app = app
  @device = device
  @xcode = options[:xcode]

  # stdio.pipe - can cause problems finding the SHA of a simulator
  rm_instruments_pipe
end
quit_simulator() click to toggle source

@!visibility private Quit any Simulator.app or iOS Simulator.app

# File lib/run_loop/core_simulator.rb, line 207
def self.quit_simulator
  RunLoop::DeviceAgent::Xcodebuild.terminate_simulator_tests

  SIMULATOR_QUIT_PROCESSES.each do |process_details|
    process_name = process_details[0]
    send_term_first = process_details[1]
    self.term_or_kill(process_name, send_term_first)
  end
end
set_language(device, lang_code) click to toggle source

@!visibility private

@param [RunLoop::Device, String] device a simulator UDID, instruments-ready

name, or a RunLoop::Device

@param [String] lang_code a language code

@raise [ArgumentError] if no device can be found matching the UDID or

instruments-ready name

@raise [ArgumentError] if device is not a simulator @raise [ArgumentError] if language_code is invalid

# File lib/run_loop/core_simulator.rb, line 336
def self.set_language(device, lang_code)
  if device.is_a?(RunLoop::Device)
    simulator = device
  else
    simulator = RunLoop::Device.device_with_identifier(device)
  end

  if simulator.physical_device?
    raise ArgumentError, "The language cannot be set on physical devices"
  end

  self.quit_simulator
  RunLoop.log_debug("Setting preferred language to '#{lang_code}'")
  simulator.simulator_set_language(lang_code)
end
set_locale(device, locale_code) click to toggle source

@!visibility private

@param [RunLoop::Device, String] device a simulator UDID, instruments-ready

name, or a RunLoop::Device.

@param [String] locale_code a locale code

@raise [ArgumentError] if no device can be found matching the UDID or

instruments-ready name

@raise [ArgumentError] if device is not a simulator @raise [ArgumentError] if locale_code is invalid

# File lib/run_loop/core_simulator.rb, line 309
def self.set_locale(device, locale_code)
  if device.is_a?(RunLoop::Device)
    simulator = device
  else
    simulator = RunLoop::Device.device_with_identifier(device)
  end

  if simulator.physical_device?
    raise ArgumentError,
      "The locale cannot be set on physical devices"
  end

  self.quit_simulator
  RunLoop.log_debug("Setting locale to '#{locale_code}'")
  simulator.simulator_set_locale(locale_code)
end
simulator_preferences_plist(pbuddy) click to toggle source

@!visibility private

Per-user CoreSimulator preferences located in ~/Library/Preferences

# File lib/run_loop/core_simulator.rb, line 248
def self.simulator_preferences_plist(pbuddy)
  if !File.exist?(PREFERENCES_PLIST)
    pbuddy.create_plist(PREFERENCES_PLIST)
  end

  PREFERENCES_PLIST
end
terminate_core_simulator_processes() click to toggle source

@!visibility private

Terminate CoreSimulator related processes. This processes can accumulate as testing proceeds and can cause instability.

# File lib/run_loop/core_simulator.rb, line 151
def self.terminate_core_simulator_processes

  start = Time.now

  self.quit_simulator

  MANAGED_PROCESSES.each do |process_name|
    send_term_first = false
    self.term_or_kill(process_name, send_term_first)
  end

  ps_name_fn = lambda do |pid|
    args = ["ps", "-o", "comm=", "-p", pid.to_s]
    out = RunLoop::Shell.run_shell_command(args)[:out]
    if out && out.strip != ""
      out.strip
    else
      "UNKNOWN PROCESS: #{pid}"
    end
  end

  term_options = { :timeout => 0.1 }
  kill_options = { :timeout => 0.0 }

  RunLoop::ProcessWaiter.pgrep_f("launchd_sim").each do |pid|
    process_name = ps_name_fn.call(pid)
    RunLoop::ProcessTerminator.new(pid, 'KILL', process_name, kill_options).kill_process
  end

  RunLoop::ProcessWaiter.pgrep_f("iPhoneSimulator").each do |pid|
    process_name = ps_name_fn.call(pid)
    RunLoop::ProcessTerminator.new(pid, 'KILL', process_name, kill_options).kill_process
  end

  RunLoop::ProcessWaiter.pgrep_f("CoreSimulator").each do |pid|
    args = ["ps", "-o", "uid=", pid.to_s]
    uid = RunLoop::Shell.run_shell_command(args)[:out].strip
    process_name = File.basename(ps_name_fn.call(pid))
    if uid != "0"

      term = RunLoop::ProcessTerminator.new(pid, 'TERM', process_name, term_options)
      killed = term.kill_process

      if !killed
        term = RunLoop::ProcessTerminator.new(pid, 'KILL', process_name, kill_options)
        term.kill_process
      end
    end
  end

  elapsed = Time.now - start
  RunLoop.log_debug("Took #{elapsed} to terminate CoreSimulator Services")
end
wait_for_simulator_state(simulator, target_state) click to toggle source

@!visibility private

Some operations, like erase, require that the simulator be ‘Shutdown’.

@param [RunLoop::Device] simulator the sim to wait for @param [String] target_state the state to wait for

# File lib/run_loop/core_simulator.rb, line 224
def self.wait_for_simulator_state(simulator, target_state)
  now = Time.now
  timeout = DEFAULT_OPTIONS[:wait_for_state_timeout]
  poll_until = now + timeout
  delay = WAIT_FOR_SIMULATOR_STATE_INTERVAL
  in_state = false
  while Time.now < poll_until
    in_state = simulator.update_simulator_state == target_state
    break if in_state
    sleep delay if delay != 0
  end

  elapsed = Time.now - now
  RunLoop.log_debug("Waited for #{elapsed} seconds for device to have state: '#{target_state}'.")

  unless in_state
    raise "Expected '#{target_state} but found '#{simulator.state}' after waiting."
  end
  in_state
end

Private Class Methods

system_app_installed?(bundle_identifier, xcode) click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 1197
def self.system_app_installed?(bundle_identifier, xcode)
  apps_dir = self.send(:system_applications_dir, xcode)
  serv_dir = self.send(:system_services_dir, xcode)

  return false if !File.exist?(apps_dir) || !File.exist?(serv_dir)

  if xcode.version_gte_90?
    black_list = [
      "AirMusic.app", "AirPodcasts.app", "AppStore.app", "Calculator.app",
      "CheckerBoard.app", "CTCarrierSpaceAuth.app", "Diagnostics.app",
      "DiagnosticsService.app", "FaceTime.app", "Feedback Assistant iOS.app",
      "FindMyFriends.app", "iBooks.app", "Magnifier.app", "MobileMail.app",
      "MobileNotes.app", "Music.app", "Podcasts.app", "PreBoard.app",
      "SoftwareUpdateUIService.app", "StoreDemoViewService.app", "TV.app",
      "Videos.app"
    ]
  else
    black_list = ["Fitness.app", "Photo Booth.app", "ScreenSharingViewService.app"]
  end

  [Dir.glob("#{apps_dir}/*.app"), Dir.glob("#{serv_dir}/*.app")].flatten.detect do |app_dir|
    basename = File.basename(app_dir)
    if black_list.include?(basename)
      false
    else
      begin
        RunLoop::App.new(app_dir).bundle_identifier == bundle_identifier
      rescue ArgumentError => _
        bundle_name = File.basename(app_dir)
        RunLoop.log_debug("Could not create an App from simulator system app: #{bundle_name}")
        nil
      end
    end
  end
end
system_applications_dir(xcode=RunLoop::Xcode.new) click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 1165
def self.system_applications_dir(xcode=RunLoop::Xcode.new)
  if xcode.version_gte_90?
    apps_dir = File.join(xcode.core_simulator_dir,
                        "Profiles", "Runtimes", "iOS.simruntime",
                        "Contents", "Resources", "RuntimeRoot",
                        "Applications")
  else
    apps_dir = File.join(xcode.developer_dir,
                        "Platforms", "iPhoneSimulator.platform",
                        "Developer", "SDKs", "iPhoneSimulator.sdk",
                        "Applications")
  end
  File.expand_path(apps_dir)
end
system_services_dir(xcode=RunLoop::Xcode.new) click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 1181
def self.system_services_dir(xcode=RunLoop::Xcode.new)
  if xcode.version_gte_90?
    apps_dir = File.join(xcode.core_simulator_dir,
                         "Profiles", "Runtimes", "iOS.simruntime",
                         "Contents", "Resources", "RuntimeRoot",
                         "System", "Library", "CoreServices")
  else
    apps_dir = File.join(xcode.developer_dir,
                        "Platforms", "iPhoneSimulator.platform",
                        "Developer", "SDKs", "iPhoneSimulator.sdk",
                        "System", "Library", "CoreServices")
  end
  File.expand_path(apps_dir)
end
term_or_kill(process_name, send_term_first) click to toggle source

Send ‘TERM’ then ‘KILL’ to allow processes to quit cleanly.

# File lib/run_loop/core_simulator.rb, line 593
def self.term_or_kill(process_name, send_term_first)
  term_options = { :timeout => 0.5 }
  kill_options = { :timeout => 0.0 }

  RunLoop::ProcessWaiter.new(process_name).pids.each do |pid|

    # We could try to determine if terminating the process will be successful
    # by asking for the parent pid and the user id.  This adds another call
    # to `ps` and does not save any time.  It is easier to simply let the
    # ProcessTerminator fail.  The downside is that a failure will appear
    # in the debug log.
    #
    # macOS is looking more like iOS.  Process names like 'mobileassetd' are
    # found in both operating systems.

    args = ["ps", "-o", "uid=", pid.to_s]
    uid = RunLoop::Shell.run_shell_command(args)[:out].strip
    if uid != "0"
      killed = false

      if send_term_first
        term = RunLoop::ProcessTerminator.new(pid, 'TERM', process_name, term_options)
        killed = term.kill_process
      end

      if !killed
        term = RunLoop::ProcessTerminator.new(pid, 'KILL', process_name, kill_options)
        term.kill_process
      end
    end
  end
end
user_app_installed?(device, bundle_identifier) click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 1234
def self.user_app_installed?(device, bundle_identifier)
  core_sim = self.new(device, bundle_identifier)
  sim_apps_dir = core_sim.send(:device_applications_dir)
  Dir.glob("#{sim_apps_dir}/**/*.app").find do |path|
    RunLoop::App.new(path).bundle_identifier == bundle_identifier
  end
end

Public Instance Methods

app_is_installed?() click to toggle source

Is this app installed?

# File lib/run_loop/core_simulator.rb, line 543
def app_is_installed?
  if installed_app_bundle_dir ||
    simctl.app_container(device, app.bundle_identifier)
    true
  else
    false
  end
end
install() click to toggle source

Install the app.

  1. If the app is not installed, it is installed.

  2. Does nothing, if the app is the same as the one that is installed.

  3. Installs the app if it is different from the installed app.

The app sandbox is not touched.

# File lib/run_loop/core_simulator.rb, line 529
def install
  installed_app_bundle = installed_app_bundle_dir

  # App is not installed. Use simctl interface to install.
  if !installed_app_bundle
    installed_app_bundle = install_app_with_simctl
  else
    ensure_app_same
  end

  installed_app_bundle
end
launch() click to toggle source

Launch the app on the simulator.

  1. If the app is not installed, it is installed.

  2. If the app is different from the app that is installed, it is installed.

# File lib/run_loop/core_simulator.rb, line 482
  def launch
    install

    # If the app is the same, install will not launch the simulator.
    # In order to launch the app, the simulator needs to be running.
    # launch_simulator ensures that the sim is launched and will not
    # relaunch it.
    launch_simulator

    tries = app_launch_retries

    RunLoop.log_debug("Trying #{tries} times to launch #{app.bundle_identifier} on #{device}")

    last_error = try_to_launch_app_n_times(tries)

    if last_error
      raise RuntimeError, %Q[
Could not launch #{app.bundle_identifier} on #{device} after trying #{tries} times:

#{last_error}:

#{last_error.message}

]
    end

    wait_for_app_launch
  end
launch_simulator(options={}) click to toggle source

Launch the simulator indicated by device.

# File lib/run_loop/core_simulator.rb, line 433
def launch_simulator(options={})
  merged_options = {
    :wait_for_stable => true
  }.merge(options)

  if !simulator_requires_relaunch?
    RunLoop.log_debug("Simulator is running and does not require a relaunch.")
    return
  end

  RunLoop::CoreSimulator.quit_simulator
  RunLoop::CoreSimulator.ensure_hardware_keyboard_connected(pbuddy)
  sim_keyboard.ensure_soft_keyboard_will_show
  sim_keyboard.ensure_keyboard_tutorial_disabled

  args = ['open', '-g', '-a', sim_app_path, '--args',
          '-CurrentDeviceUDID', device.udid,
          "-ConnectHardwareKeyboard", "0",
          "-DeviceBootTimeout", "120",
          # Yes, this is the argument even though it is not spelled correctly
          "-DetatchOnAppQuit", "0",
          "-DetachOnWindowClose", "0",
          "LAUNCHED_BY_RUN_LOOP"]

  RunLoop.log_debug("Launching #{device} with:")
  RunLoop.log_unix_cmd("xcrun #{args.join(' ')}")

  start_time = Time.now

  pid = Process.spawn('xcrun', *args)
  Process.detach(pid)

  options = { :timeout => 5, :raise_on_timeout => true }
  RunLoop::ProcessWaiter.new(sim_name, options).wait_for_any

  if merged_options[:wait_for_stable]
    device.simulator_wait_for_stable_state
  end

  elapsed = Time.now - start_time
  RunLoop.log_debug("Took #{elapsed} seconds to launch the simulator")

  true
end
reset_app_sandbox() click to toggle source

Resets the app sandbox.

Does nothing if the app is not installed.

# File lib/run_loop/core_simulator.rb, line 555
def reset_app_sandbox
  return true if !app_is_installed?

  RunLoop::CoreSimulator.quit_simulator
  RunLoop::CoreSimulator.wait_for_simulator_state(device, "Shutdown")

  reset_app_sandbox_internal
end
simctl() click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 422
def simctl
  @simctl ||= RunLoop::Simctl.new
end
simulator_requires_relaunch?() click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 427
def simulator_requires_relaunch?
  [simulator_state_requires_relaunch?,
   running_apps_require_relaunch?].any?
end
uninstall_app_and_sandbox() click to toggle source

Uninstalls the app and clears the sandbox.

# File lib/run_loop/core_simulator.rb, line 565
def uninstall_app_and_sandbox
  return true if !app_is_installed?

  uninstall_app_with_simctl
  true
end
wait_for_app_launch() click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 512
def wait_for_app_launch
  options = {
    :timeout => 10,
    :raise_on_timeout => true
  }
  RunLoop::ProcessWaiter.new(app.executable_name, options).wait_for_any
  device.simulator_wait_for_stable_state
  true
end

Private Instance Methods

app_documents_dir() click to toggle source

The Documents directory in the sandbox.

# File lib/run_loop/core_simulator.rb, line 969
def app_documents_dir
  base_dir = app_sandbox_dir
  if base_dir.nil?
    nil
  else
    File.join(base_dir, 'Documents')
  end
end
app_launch_retries() click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 774
def app_launch_retries
  DEFAULT_OPTIONS[:app_launch_retries]
end
app_library_dir() click to toggle source

The Library directory in the sandbox.

# File lib/run_loop/core_simulator.rb, line 949
def app_library_dir
  base_dir = app_sandbox_dir
  if base_dir.nil?
    nil
  else
    File.join(base_dir, 'Library')
  end
end
app_library_preferences_dir() click to toggle source

The Library/Preferences directory in the sandbox.

# File lib/run_loop/core_simulator.rb, line 959
def app_library_preferences_dir
  base_dir = app_library_dir
  if base_dir.nil?
    nil
  else
    File.join(base_dir, 'Preferences')
  end
end
app_sandbox_dir() click to toggle source

The sandbox directory for the app.

~/Library/Developer/CoreSimulator/Devices/<UDID>/Containers/Data/Application

Contains Library, Documents, and tmp directories.

# File lib/run_loop/core_simulator.rb, line 925
def app_sandbox_dir
  app_install_dir = installed_app_bundle_dir
  return nil if app_install_dir.nil?
  if sdk_gte_8?
    app_sandbox_dir_sdk_gte_8
  else
    app_install_dir
  end
end
app_sandbox_dir_sdk_gte_8() click to toggle source
# File lib/run_loop/core_simulator.rb, line 935
def app_sandbox_dir_sdk_gte_8
  containers_data_dir = File.join(device_data_dir, 'Containers', 'Data', 'Application')
  apps = Dir.glob("#{containers_data_dir}/**/#{METADATA_PLIST}")
  match = apps.find do |metadata_plist|
    pbuddy.plist_read('MCMMetadataIdentifier', metadata_plist) == app.bundle_identifier
  end
  if match
    File.dirname(match)
  else
    nil
  end
end
app_tmp_dir() click to toggle source

The tmp directory in the sandbox.

# File lib/run_loop/core_simulator.rb, line 979
def app_tmp_dir
  base_dir = app_sandbox_dir
  if base_dir.nil?
    nil
  else
    File.join(base_dir, 'tmp')
  end
end
clear_device_launch_csstore() click to toggle source

Required after when installing and uninstalling.

# File lib/run_loop/core_simulator.rb, line 994
def clear_device_launch_csstore
  glob = File.join(device_caches_dir, "com.apple.LaunchServices-*.csstore")
  Dir.glob(glob) do | ccstore |
    FileUtils.rm_f ccstore
  end
end
complete_app_install?(app_bundle_dir) click to toggle source

Detect an incomplete app installation.

# File lib/run_loop/core_simulator.rb, line 1050
def complete_app_install?(app_bundle_dir)
  base_dir = File.dirname(app_bundle_dir)
  plist = File.join(base_dir, METADATA_PLIST)
  File.exist?(plist)
end
device_agent_launched_by_xcode?(running_apps) click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 841
def device_agent_launched_by_xcode?(running_apps)
  process_info = running_apps["XCTRunner"] || running_apps["DeviceAgent-Runner"]
  return false if !process_info

  process_info[:args][/CBX_LAUNCHED_BY_XCODE/]
end
device_applications_dir() click to toggle source

The applications directory for the device.

~/Library/Developer/CoreSimulator/Devices/<UDID>/Containers/Bundle/Application

# File lib/run_loop/core_simulator.rb, line 910
def device_applications_dir
  @device_app_dir ||= lambda do
    if sdk_gte_8?
      File.join(device_data_dir, 'Containers', 'Bundle', 'Application')
    else
      File.join(device_data_dir, 'Applications')
    end
  end.call
end
device_caches_dir() click to toggle source

A cache of installed apps on the device.

# File lib/run_loop/core_simulator.rb, line 989
def device_caches_dir
  @device_caches_dir ||= File.join(device_data_dir, 'Library', 'Caches')
end
device_data_dir() click to toggle source

The data directory for the the device.

~/Library/Developer/CoreSimulator/Devices/<UDID>/data

# File lib/run_loop/core_simulator.rb, line 903
def device_data_dir
  @device_data_dir ||= File.join(CORE_SIMULATOR_DEVICE_DIR, device.udid, 'data')
end
ensure_app_same() click to toggle source
  1. Does nothing if the app is not installed.

  2. Does nothing if the app the same as the app that is installed

  3. Installs app if it is different from the installed app

# File lib/run_loop/core_simulator.rb, line 1072
def ensure_app_same
  installed_app_bundle = installed_app_bundle_dir

  if !installed_app_bundle
    RunLoop.log_debug("App: #{app} is not installed")
    return true
  end

  installed_sha = installed_app_sha1
  app_sha = app.sha1

  if installed_sha == app_sha
    RunLoop.log_debug("Installed app is the same as #{app}")
    return true
  end

  RunLoop.log_debug("The app you are testing is not the same as the app that is installed.")
  RunLoop.log_debug("  Installed app SHA: #{installed_sha}")
  RunLoop.log_debug("  App to launch SHA: #{app_sha}")
  RunLoop.log_debug("Will install #{app}")

  uninstall_app_with_simctl
  install_app_with_simctl
  true
end
ensure_complete_app_installation(app_bundle_dir) click to toggle source

Cleans up bad installations of an app. For unknown reasons, an app bundle can exist, but be unrecognized by CoreSimulator. If we detect a case like this, we need to clean up the installation.

# File lib/run_loop/core_simulator.rb, line 1035
def ensure_complete_app_installation(app_bundle_dir)
  return nil if app_bundle_dir.nil?
  return app_bundle_dir if complete_app_install?(app_bundle_dir)

  # Remove the directory that contains the app bundle
  base_dir = File.dirname(app_bundle_dir)
  FileUtils.rm_rf(base_dir)

  # Clean up Containers/Data/Application
  remove_stale_data_containers

  nil
end
install_app_with_simctl() click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 723
def install_app_with_simctl
  launch_simulator

  app_size = RunLoop::Directory.size(app.path, :mb)
  sim_size = device.simulator_size_on_disk
  target_size = sim_size + app_size

  timeout = DEFAULT_OPTIONS[:install_app_timeout]
  simctl.install(device, app, timeout)

  current_size = device.simulator_size_on_disk
  start = Time.now
  while current_size <= target_size && Time.now < start + 5
    sleep(0.5)
    current_size = device.simulator_size_on_disk
  end

  elapsed = Time.now - start
  if current_size > target_size
    RunLoop.log_debug("Waited for #{elapsed} seconds for app to install")
  else
    RunLoop.log_debug("Timed out after #{elapsed} seconds for app to install")
    RunLoop.log_debug("Expected sim size #{current_size} >= #{target_size}")
  end

  installed_app_bundle_dir
end
installed_app_bundle_dir() click to toggle source

Returns the path to the installed app bundle directory (.app).

If this method returns nil, the app is not installed.

# File lib/run_loop/core_simulator.rb, line 1019
def installed_app_bundle_dir
  sim_app_dir = device_applications_dir
  return nil if !File.exist?(sim_app_dir)

  app_bundle_dir = Dir.glob("#{sim_app_dir}/**/*.app").find do |path|
    RunLoop::App.new(path).bundle_identifier == app.bundle_identifier
  end

  app_bundle_dir = ensure_complete_app_installation(app_bundle_dir)

  app_bundle_dir
end
installed_app_sha1() click to toggle source

The sha1 of the installed app.

# File lib/run_loop/core_simulator.rb, line 1002
def installed_app_sha1
  installed_bundle = installed_app_bundle_dir
  if installed_bundle
    RunLoop::Directory.directory_digest(installed_bundle)
  else
    nil
  end
end
launch_app_with_simctl() click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 752
def launch_app_with_simctl
  timeout = DEFAULT_OPTIONS[:launch_app_timeout]
  simctl.launch(device, app, timeout)
end
remove_stale_data_containers() click to toggle source

Remove stale data directories that might have appeared as a result of an incomplete app installation. See ensure_complete_app_installation

# File lib/run_loop/core_simulator.rb, line 1059
def remove_stale_data_containers
  containers_data_dir = File.join(device_data_dir, "Containers", "Data", "Application")
  apps = Dir.glob("#{containers_data_dir}/**/#{METADATA_PLIST}")
  apps.each do |metadata_plist|
    if pbuddy.plist_read("MCMMetadataIdentifier", metadata_plist) == app.bundle_identifier
      FileUtils.rm_rf(File.dirname(metadata_plist))
    end
  end
end
reset_app_sandbox_internal() click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 1154
def reset_app_sandbox_internal
  reset_app_sandbox_internal_shared

  if sdk_gte_8?
    reset_app_sandbox_internal_sdk_gte_8
  else
    reset_app_sandbox_internal_sdk_lt_8
  end
end
reset_app_sandbox_internal_sdk_gte_8() click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 1107
def reset_app_sandbox_internal_sdk_gte_8
  lib_dir = app_library_dir
  RunLoop::Directory.recursive_glob_for_entries(lib_dir).each do |entry|
    if entry.include?('Preferences')
      # nop
    else
      if File.exist?(entry)
        FileUtils.rm_rf(entry)
      end
    end
  end

  prefs_dir = app_library_preferences_dir
  protected = ['com.apple.UIAutomation.plist',
               'com.apple.UIAutomationPlugIn.plist']
  RunLoop::Directory.recursive_glob_for_entries(prefs_dir).each do |entry|
    unless protected.include?(File.basename(entry))
      if File.exist?(entry)
        FileUtils.rm_rf entry
      end
    end
  end
end
reset_app_sandbox_internal_sdk_lt_8() click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 1132
def reset_app_sandbox_internal_sdk_lt_8
  prefs_dir = app_library_preferences_dir
  RunLoop::Directory.recursive_glob_for_entries(prefs_dir).each do |entry|
    if entry.end_with?('.GlobalPreferences.plist') ||
          entry.end_with?('com.apple.PeoplePicker.plist')
      # nop
    else
      if File.exist?(entry)
        FileUtils.rm_rf entry
      end
    end
  end

  # app preferences lives in device Library/Preferences
  device_prefs_dir = File.join(app_sandbox_dir, 'Library', 'Preferences')
  app_prefs_plist = File.join(device_prefs_dir, "#{app.bundle_identifier}.plist")
  if File.exist?(app_prefs_plist)
    FileUtils.rm_rf(app_prefs_plist)
  end
end
reset_app_sandbox_internal_shared() click to toggle source

Shared tasks across CoreSimulators iOS 7 and > iOS 7

# File lib/run_loop/core_simulator.rb, line 1099
def reset_app_sandbox_internal_shared
  [app_documents_dir, app_tmp_dir].each do |dir|
    FileUtils.rm_rf dir
    FileUtils.mkdir dir
  end
end
rm_instruments_pipe() click to toggle source

@!visibility private

This stdio.pipe file causes problems when checking the size and taking the checksum of the core simulator directory.

# File lib/run_loop/core_simulator.rb, line 582
def rm_instruments_pipe
  device_tmp_dir = File.join(device_data_dir, 'tmp')
  Dir.glob("#{device_tmp_dir}/instruments_*/stdio.pipe") do |file|
    if File.exist?(file)
      RunLoop.log_debug("Deleting #{file}")
      FileUtils.rm_rf(file)
    end
  end
end
running_apps_require_relaunch?() click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 849
def running_apps_require_relaunch?
  running_apps = device.simulator_running_app_details

  if running_apps.empty?
    RunLoop.log_debug("Simulator relaunch not required: no running apps")
    return false
  end

  # DeviceAgent is running, but it was launched by Xcode.
  if device_agent_launched_by_xcode?(running_apps)
    RunLoop.log_debug("Simulator relaunch required: XCTRunner is controlled by Xcode")
    return true
  end

  # Why?
  # No app was passed to initializer.
  if app.nil?
    RunLoop.log_debug("Simulator relaunch required: no app was passed to CoreSimulator.new")
    return true
  end

  # AUT is running, but it was not launched by DeviceAgent.
  app_name = app.executable_name
  if running_apps[app_name]
    launch_arg = RunLoop::DeviceAgent::Client::AUT_LAUNCHED_BY_RUN_LOOP_ARG
    if !running_apps[app_name][:args][/#{launch_arg}/]
      RunLoop.log_debug("Simulator relaunch required: AUT is running, but not launched by run-loop")
      return true
    end
  end

  # This is the UITest behavior.  UITest does not inspect the simulator for
  # system apps that are running - it only checks for running user apps.
  #
  # I don't think this condition is necessary, so we'll skip it for now, but
  # capture there is a difference between UITest and run-loop.
  #
  # There is some other application running on the simulator.
  # running_apps.delete("XCTRunner")
  # running_apps.delete(app_name)
  #
  # if running_apps.empty?
  #   RunLoop.log_debug("Simulator relaunch not required: only XCTRunner and AUT are running")
  #   false
  # else
  #   RunLoop.log_debug("Simulator relaunch required: other applications are running")
  #   true
  # end
  false
end
running_simulator_details() click to toggle source

@!visibility private

@return [Hash] details about the running simulator.

# File lib/run_loop/core_simulator.rb, line 648
  def running_simulator_details
    process_name = sim_app_path

    args = ["ps", "x", "-o", "pid=,command="]
    hash = run_shell_command(args)

    exit_status = hash[:exit_status]
    if exit_status != 0
      raise RuntimeError,
%Q{Could not find the process details of #{sim_name} with:

#{args.join(" ")}

Command exited with status: #{exit_status}

  '#{hash[:out]}'
}
    end

    if hash[:out].nil? || hash[:out] == ""
       raise RuntimeError,
%Q{Could not find the process details of #{sim_name} with:

#{args.join(" ")}

Command had no output.
}
    end

    lines = hash[:out].split($-0)

    match = lines.detect do |line|
      line[/#{process_name}/]
    end

    return {} if match.nil?

    hash = {}

    pid = match.split(" ").first.strip.to_i
    hash[:pid] = pid

    hash[:launched_by_run_loop] = match[/LAUNCHED_BY_RUN_LOOP/]

    hash
  end
same_sha1_as_installed?() click to toggle source

Is the app that is install the same as the one we have in hand?

# File lib/run_loop/core_simulator.rb, line 1012
def same_sha1_as_installed?
  app.sha1 == installed_app_sha1
end
sdk_gte_8?() click to toggle source

Required for support of iOS 7 CoreSimulators. Can be removed when Xcode support is dropped.

# File lib/run_loop/core_simulator.rb, line 799
def sdk_gte_8?
  device.version >= RunLoop::Version.new('8.0')
end
sim_app_path() click to toggle source

@!visibility private Returns the path to the current simulator.

@return [String] The path to the simulator app for the current version of

Xcode.
# File lib/run_loop/core_simulator.rb, line 639
def sim_app_path
  @sim_app_path ||= begin
    "#{xcode.developer_dir}/Applications/#{sim_name}.app"
  end
end
sim_name() click to toggle source

Returns the current simulator name.

@return [String] A String suitable for searching for a pid, quitting, or

launching the current simulator.
# File lib/run_loop/core_simulator.rb, line 630
def sim_name
  @sim_name ||= "Simulator"
end
simulator_state_requires_relaunch?() click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 804
def simulator_state_requires_relaunch?
  running_sim_details = running_simulator_details

  # Simulator is not running.
  if !running_sim_details[:pid]
    RunLoop.log_debug("Simulator relaunch required: simulator is not running.")
    return true
  end

  # Simulator is running, but run-loop did not launch it.
  if !running_sim_details[:launched_by_run_loop]
    RunLoop.log_debug("Simulator relaunch required: simulator was not launched by run_loop")
    return true
  end

  if !RunLoop::CoreSimulator.hardware_keyboard_connected?(pbuddy)
    RunLoop.log_debug("Simulator relaunch required: hardware keyboard not connected.")
    return true
  end

  if !sim_keyboard.soft_keyboard_will_show?
    RunLoop.log_debug("Simulator relaunch required:  software keyboard is minimized")
    return true
  end

  # Simulator is running, run_loop launched it, but it is not Booted.
  device.update_simulator_state
  if device.state == "Booted"
    RunLoop.log_debug("Simulator relaunch not required: simulator has state 'Booted'")
    false
  else
    RunLoop.log_debug("Simulator relaunch required: simulator does not have state 'Booted'")
    true
  end
end
try_to_launch_app() click to toggle source

@!visibility private

Returns nil if launch_app_with_simctl succeeds and the error if it fails.

# File lib/run_loop/core_simulator.rb, line 760
def try_to_launch_app
  begin
    launch_app_with_simctl
    nil
  rescue RuntimeError, RunLoop::Xcrun::TimeoutError  => error
    # Simulator is probably in a bad state.  Restart the service.
    RunLoop::CoreSimulator.terminate_core_simulator_processes
    Kernel.sleep(0.5)
    launch_simulator
    error
  end
end
try_to_launch_app_n_times(tries) click to toggle source

@!visibility private

Returns nil if launch_app_with_simctl succeeds and the error if it fails.

# File lib/run_loop/core_simulator.rb, line 781
def try_to_launch_app_n_times(tries)
  last_error = nil

  tries.times do |try|
    # Terminates CoreSimulatorService on failures and launches the simulator again.
    # Returns nil if app launched.
    # Returns rescued Runtime or Timeout errors.
    last_error = try_to_launch_app

    break if last_error.nil?
    RunLoop.log_debug("Failed to launch app on try #{try + 1} of #{tries}.")
  end

  last_error
end
uninstall_app_with_simctl() click to toggle source

@!visibility private

# File lib/run_loop/core_simulator.rb, line 696
def uninstall_app_with_simctl
  launch_simulator

  app_size = RunLoop::Directory.size(app.path, :mb)
  sim_size = device.simulator_size_on_disk
  target_size = sim_size - app_size + 5

  timeout = DEFAULT_OPTIONS[:install_app_timeout]
  simctl.uninstall(device, app, timeout)

  current_size = device.simulator_size_on_disk
  start = Time.now
  while current_size > target_size && Time.now < start + 5
    sleep(0.5)
    current_size = device.simulator_size_on_disk
  end

  elapsed = Time.now - start
  if current_size <= target_size
    RunLoop.log_debug("Waited for #{elapsed} seconds for app to uninstall")
  else
    RunLoop.log_debug("Timed out after #{elapsed} seconds for app to uninstall")
    RunLoop.log_debug("Expected sim size #{current_size} < #{target_size}")
  end
end