module RunLoop # A class for interacting with the instruments command-line tool # # @note All instruments commands are run in the context of `xcrun`. # # @todo Detect Instruments.app is running and pop an alert. class Instruments # Returns an Array of instruments process ids. # # @note The `block` parameter is included for legacy API and will be # deprecated. Replace your existing calls with with .each or .map. The # block argument makes this method hard to mock. # @return [Array<Integer>] An array of instruments process ids. def instruments_pids(&block) pids = pids_from_ps_output if block_given? pids.each do |pid| block.call(pid) end else pids end end # Are there any instruments processes running? # @return [Boolean] True if there is are any instruments processes running. def instruments_running? instruments_pids.count > 0 end # Send a kill signal to any running `instruments` processes. # # Only one instruments process can be running at any one time. # # @param [RunLoop::XCTools] xcode_tools The Xcode tools to use to determine # what version of Xcode is active. def kill_instruments(xcode_tools = RunLoop::XCTools.new) kill_signal = kill_signal xcode_tools # It is difficult to test using a block. instruments_pids.each do |pid| begin if ENV['DEBUG'] == '1' puts "Sending '#{kill_signal}' to instruments process '#{pid}'" end Process.kill(kill_signal, pid.to_i) Process.wait(pid, Process::WNOHANG) rescue Exception => e if ENV['DEBUG'] == '1' puts "Could not kill and wait for process '#{pid.to_i}' - ignoring exception '#{e}'" end end # Process.wait or `wait` here is pointless. The pid may or may not be # a child of this Process. begin if ENV['DEBUG'] == '1' puts "Waiting for instruments '#{pid}' to terminate" end wait_for_process_to_terminate(pid, {:timeout => 2.0}) rescue Exception => e if ENV['DEBUG'] == '1' puts "Ignoring #{e.message}" end end end end private # @!visibility private # When run from calabash, expect this: # # ``` # $ ps x -o pid,command | grep -v grep | grep instruments # 98081 sh -c xcrun instruments -w "43be3f89d9587e9468c24672777ff6241bd91124" < args > # 98082 /Xcode/6.0.1/Xcode.app/Contents/Developer/usr/bin/instruments -w < args > # ``` # When run from run-loop (via rspec), expect this: # # ``` # $ ps x -o pid,command | grep -v grep | grep instruments # 98082 /Xcode/6.0.1/Xcode.app/Contents/Developer/usr/bin/instruments -w < args > FIND_PIDS_CMD = 'ps x -o pid,comm | grep -v grep | grep instruments' # @!visibility private # # Executes `ps_cmd` to find instruments processes and returns the result. # # @param [String] ps_cmd The Unix ps command to execute to find instruments # processes. # @return [String] A ps-style list of process details. The details returned # are controlled by the `ps_cmd`. def ps_for_instruments(ps_cmd=FIND_PIDS_CMD) `#{ps_cmd}`.strip end # @!visibility private # Is the process described an instruments process? # # @param [String] ps_details Details about a process as returned by `ps` # @return [Boolean] True if the details describe an instruments process. def is_instruments_process?(ps_details) return false if ps_details.nil? (ps_details[/\/usr\/bin\/instruments/, 0] or ps_details[/sh -c xcrun instruments/, 0]) != nil end # @!visibility private # Extracts an Array of integer process ids from the output of executing # the Unix `ps_cmd`. # # @param [String] ps_cmd The Unix `ps` command used to find instruments # processes. # @return [Array<Integer>] An array of integer pids for instruments # processes. Returns an empty list if no instruments process are found. def pids_from_ps_output(ps_cmd=FIND_PIDS_CMD) ps_output = ps_for_instruments(ps_cmd) lines = ps_output.lines("\n").map { |line| line.strip } lines.map do |line| tokens = line.strip.split(' ').map { |token| token.strip } pid = tokens.fetch(0, nil) process_description = tokens[1..-1].join(' ') if is_instruments_process? process_description pid.to_i else nil end end.compact end # @!visibility private # The kill signal should be sent to instruments. # # When testing against iOS 8, sending -9 or 'TERM' causes the ScriptAgent # process on the device to emit the following error until the device is # rebooted. # # ``` # MobileGestaltHelper[909] <Error>: libMobileGestalt MobileGestalt.c:273: server_access_check denied access to question UniqueDeviceID for pid 796 # ScriptAgent[796] <Error>: libMobileGestalt MobileGestaltSupport.m:170: pid 796 (ScriptAgent) does not have sandbox access for re6Zb+zwFKJNlkQTUeT+/w and IS NOT appropriately entitled # ScriptAgent[703] <Error>: libMobileGestalt MobileGestalt.c:534: no access to UniqueDeviceID (see <rdar://problem/11744455>) # ``` # # @see https://github.com/calabash/run_loop/issues/34 # # @param [RunLoop::XCTools] xcode_tools The Xcode tools to use to determine # what version of Xcode is active. # @return [String] Either 'QUIT' or 'TERM', depending on the Xcode # version. def kill_signal(xcode_tools = RunLoop::XCTools.new) xcode_tools.xcode_version_gte_6? ? 'QUIT' : 'TERM' end # @!visibility private # Wait for Unix process with id `pid` to terminate. # # @param [Integer] pid The id of the process we are waiting on. # @param [Hash] options Values to control the behavior of this method. # @option options [Float] :timeout (2.0) How long to wait for the process to # terminate. # @option options [Float] :interval (0.1) The polling interval. # @option options [Boolean] :raise_on_no_terminate (false) Should an error # be raised if process does not terminate. # @raise [RuntimeError] If process does not terminate and # options[:raise_on_no_terminate] is truthy. def wait_for_process_to_terminate(pid, options={}) default_opts = {:timeout => 2.0, :interval => 0.1, :raise_on_no_terminate => false} merged_opts = default_opts.merge(options) cmd = "ps #{pid} -o pid | grep #{pid}" poll_until = Time.now + merged_opts[:timeout] delay = merged_opts[:interval] has_terminated = false while Time.now < poll_until has_terminated = `#{cmd}`.strip == '' break if has_terminated sleep delay end if merged_opts[:raise_on_no_terminate] and not has_terminated details = `ps -p #{pid} -o pid,comm | grep #{pid}`.strip raise RuntimeError, "Waited #{merged_opts[:timeout]} s for process '#{details}' to terminate" end end end end