class RunLoop::DylibInjector

@!visibility private

This is experimental.

Injects dylibs into running executables using lldb.

Constants

OPTIONS

Options for controlling dylib injection

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::DylibInjector::OPTIONS = 10.0

RETRY_OPTIONS

Options for controlling how often to retry dylib injection.

Try 3 times for 10 seconds each try with a sleep of 2 seconds between tries.

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::DylibInjector::RETRY_OPTIONS = 60

Attributes

dylib_path[R]

@!attribute [r] dylib_path The path to the dylib that is to be injected. @return [String] The dylib_path

process_name[R]

@!attribute [r] process_name The name of the process to inject the dylib into. This should be obtained

by inspecting the Info.plist in the app bundle.

@return [String] The process_name

xcrun[R]

@!visibility private

Public Class Methods

dylib_path_from_options(options) click to toggle source

Extracts the value of :inject_dylib from options Hash. @param options [Hash] arguments passed to {RunLoop.run} @return [String, nil] If the options contains :inject_dylibs and it is a

path to a dylib that exists, return the path.  Otherwise return nil or
raise an error.

@raise [RuntimeError] If :inject_dylib points to a path that does not exist. @raise [ArgumentError] If :inject_dylib is not a String.

# File lib/run_loop/dylib_injector.rb, line 55
    def self.dylib_path_from_options(options)
      inject_dylib = options.fetch(:inject_dylib, nil)
      return nil if inject_dylib.nil?
      if !inject_dylib.is_a? String
        raise ArgumentError, %Q[

Expected :inject_dylib to be a path to a dylib, but found '#{inject_dylib}'

]
      end
      dylib_path = File.expand_path(inject_dylib)
      unless File.exist?(dylib_path)
        raise "Cannot load dylib.  The file '#{dylib_path}' does not exist."
      end
      dylib_path
    end
new(process_name, dylib_path) click to toggle source

Create a new dylib injector. @param [String] process_name The name of the process to inject the dylib

into.  This should be obtained by inspecting the Info.plist in the app
bundle.

@param [String] dylib_path The path the dylib to inject.

# File lib/run_loop/dylib_injector.rb, line 91
def initialize(process_name, dylib_path)
  @process_name = process_name
  @dylib_path = Shellwords.shellescape(dylib_path)
end

Public Instance Methods

inject_dylib(timeout) click to toggle source

Injects a dylib into a a currently running process.

# File lib/run_loop/dylib_injector.rb, line 101
def inject_dylib(timeout)
  RunLoop.log_debug("Starting lldb injection with a timeout of #{timeout} seconds")

  script_path = write_script

  start = Time.now

  options = {
    :timeout => timeout,
    :log_cmd => true
  }

  hash = nil
  success = false
  begin
    hash = xcrun.run_command_in_context(["lldb", "--no-lldbinit", "--source", script_path], options)
    pid = hash[:pid]
    exit_status = hash[:exit_status]
    success = exit_status == 0

    RunLoop.log_debug("lldb '#{pid}' exited with value '#{exit_status}'.")

    success = exit_status == 0
    elapsed = Time.now - start

    if success
      RunLoop.log_debug("Took #{elapsed} seconds for lldb to inject calabash dylib.")
    else
      RunLoop.log_debug("Could not inject dylib after #{elapsed} seconds.")
      if hash[:out]
        hash[:out].split("\n").each do |line|
          RunLoop.log_debug(line)
        end
      else
        RunLoop.log_debug("lldb returned no output to stdout or stderr")
      end
    end
  rescue RunLoop::Xcrun::TimeoutError
    elapsed = Time.now - start
    RunLoop.log_debug("lldb tried for #{elapsed} seconds to inject calabash dylib before giving up.")
  end

  success
end
retriable_inject_dylib(options={}) click to toggle source
# File lib/run_loop/dylib_injector.rb, line 146
def retriable_inject_dylib(options={})
  delay = OPTIONS[:injection_delay]
  RunLoop.log_debug("Delaying dylib injection by #{delay} seconds to allow app to finish launching")
  sleep(delay)

  merged_options = RETRY_OPTIONS.merge(options)

  tries = merged_options[:tries]
  timeout = merged_options[:timeout]
  interval = merged_options[:interval]

  success = false

  tries.times do

    success = inject_dylib(timeout)
    break if success

    sleep(interval)
  end

  if !success
    raise RuntimeError, "Could not inject dylib"
  end
  success
end

Private Instance Methods

write_script() click to toggle source
# File lib/run_loop/dylib_injector.rb, line 175
def write_script
  script = File.join(DotDir.directory, "inject-dylib.lldb")

  if File.exist?(script)
    FileUtils.rm_rf(script)
  end

  File.open(script, "w") do |file|
    file.write("process attach -n \"#{process_name}\"\n")
    file.write("expr (void*)dlopen(\"#{dylib_path}\", 0x2)\n")
    file.write("detach\n")
    file.write("exit\n")
  end

  script
end