run_loop 1.2.6 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ module RunLoop
2
+ class Environment
3
+
4
+ # Returns the user's Unix uid.
5
+ # @return [Integer] The user's Unix uid as an integer.
6
+ def self.uid
7
+ `id -u`.strip.to_i
8
+ end
9
+
10
+ # Returns true if debugging is enabled.
11
+ def self.debug?
12
+ ENV['DEBUG'] == '1'
13
+ end
14
+ end
15
+ end
@@ -23,7 +23,8 @@ module RunLoop
23
23
  # The directory where the cache is stored.
24
24
  # @return [String] Expanded path to the default cache directory.
25
25
  def self.default_directory
26
- File.expand_path('/tmp/run-loop-host-cache')
26
+ uid = RunLoop::Environment.uid
27
+ File.expand_path("/tmp/com.xamarin.calabash.run-loop/host-cache/#{uid}")
27
28
  end
28
29
 
29
30
  # The default cache.
@@ -3,8 +3,6 @@ module RunLoop
3
3
  # A class for interacting with the instruments command-line tool
4
4
  #
5
5
  # @note All instruments commands are run in the context of `xcrun`.
6
- #
7
- # @todo Detect Instruments.app is running and pop an alert.
8
6
  class Instruments
9
7
 
10
8
  # Returns an Array of instruments process ids.
@@ -38,31 +36,11 @@ module RunLoop
38
36
  # what version of Xcode is active.
39
37
  def kill_instruments(xcode_tools = RunLoop::XCTools.new)
40
38
  kill_signal = kill_signal xcode_tools
41
- # It is difficult to test using a block.
42
39
  instruments_pids.each do |pid|
43
- begin
44
- if ENV['DEBUG'] == '1' or ENV['DEBUG_UNIX_CALLS'] == '1'
45
- puts "Sending '#{kill_signal}' to instruments process '#{pid}'"
46
- end
47
- Process.kill(kill_signal, pid.to_i)
48
- Process.wait(pid, Process::WNOHANG)
49
- rescue Exception => e
50
- if ENV['DEBUG'] == '1' or ENV['DEBUG_UNIX'] == '1'
51
- puts "Could not kill and wait for process '#{pid.to_i}' - ignoring exception '#{e}'"
52
- end
53
- end
54
-
55
- # Process.wait or `wait` here is pointless. The pid may or may not be
56
- # a child of this Process.
57
- begin
58
- if ENV['DEBUG'] == '1' or ENV['DEBUG_UNIX_CALLS'] == '1'
59
- puts "Waiting for instruments '#{pid}' to terminate"
60
- end
61
- wait_for_process_to_terminate(pid, {:timeout => 2.0})
62
- rescue Exception => e
63
- if ENV['DEBUG'] == '1' or ENV['DEBUG_UNIX_CALLS'] == '1'
64
- puts "Ignoring #{e.message}"
65
- end
40
+ terminator = RunLoop::ProcessTerminator.new(pid, kill_signal, 'instruments')
41
+ unless terminator.kill_process
42
+ terminator = RunLoop::ProcessTerminator.new(pid, 'KILL', 'instruments')
43
+ terminator.kill_process
66
44
  end
67
45
  end
68
46
  end
@@ -80,8 +58,59 @@ module RunLoop
80
58
  end
81
59
  end
82
60
 
61
+ # Spawn a new instruments process in the context of `xcrun` and detach.
62
+ #
63
+ # @param [String] automation_template The template instruments will use when
64
+ # launching the application.
65
+ # @param [Hash] options The launch options.
66
+ # @param [String] log_file The file to log to.
67
+ # @return [Integer] Returns the process id of the instruments process.
68
+ # @todo Do I need to enumerate the launch options in the docs?
69
+ # @todo Should this raise errors?
70
+ # @todo Is this jruby compatible?
71
+ def spawn(automation_template, options, log_file)
72
+ splat_args = spawn_arguments(automation_template, options)
73
+ if ENV['DEBUG'] == '1'
74
+ puts "#{Time.now} xcrun #{splat_args.join(' ')} >& #{log_file}"
75
+ $stdout.flush
76
+ end
77
+ pid = Process.spawn('xcrun', *splat_args, {:out => log_file, :err => log_file})
78
+ Process.detach(pid)
79
+ pid.to_i
80
+ end
81
+
83
82
  private
84
83
 
84
+ # @!visibility private
85
+ # Parses the run-loop options hash into an array of arguments that can be
86
+ # passed to `Process.spawn` to launch instruments.
87
+ def spawn_arguments(automation_template, options)
88
+ array = ['instruments']
89
+ array << '-w'
90
+ array << options[:udid]
91
+
92
+ trace = options[:results_dir_trace]
93
+ if trace
94
+ array << '-D'
95
+ array << trace
96
+ end
97
+
98
+ array << '-t'
99
+ array << automation_template
100
+
101
+ array << options[:bundle_dir_or_bundle_id]
102
+
103
+ {
104
+ 'UIARESULTSPATH' => options[:results_dir],
105
+ 'UIASCRIPT' => options[:script]
106
+ }.each do |key, value|
107
+ array << '-e'
108
+ array << key
109
+ array << value
110
+ end
111
+ array + options.fetch(:args, [])
112
+ end
113
+
85
114
  # @!visibility private
86
115
  #
87
116
  # ```
@@ -96,7 +125,7 @@ module RunLoop
96
125
  # $ ps x -o pid,command | grep -v grep | grep instruments
97
126
  # 98082 /Xcode/6.0.1/Xcode.app/Contents/Developer/usr/bin/instruments -w < args >
98
127
  # ```
99
- FIND_PIDS_CMD = 'ps x -o pid,command | grep -v grep | grep instruments'
128
+ INSTRUMENTS_FIND_PIDS_CMD = 'ps x -o pid,command | grep -v grep | grep instruments'
100
129
 
101
130
  # @!visibility private
102
131
  #
@@ -106,7 +135,7 @@ module RunLoop
106
135
  # processes.
107
136
  # @return [String] A ps-style list of process details. The details returned
108
137
  # are controlled by the `ps_cmd`.
109
- def ps_for_instruments(ps_cmd=FIND_PIDS_CMD)
138
+ def ps_for_instruments(ps_cmd=INSTRUMENTS_FIND_PIDS_CMD)
110
139
  `#{ps_cmd}`.strip
111
140
  end
112
141
 
@@ -117,8 +146,7 @@ module RunLoop
117
146
  # @return [Boolean] True if the details describe an instruments process.
118
147
  def is_instruments_process?(ps_details)
119
148
  return false if ps_details.nil?
120
- (ps_details[/\/usr\/bin\/instruments/, 0] or
121
- ps_details[/sh -c xcrun instruments/, 0]) != nil
149
+ ps_details[/\/usr\/bin\/instruments/, 0] != nil
122
150
  end
123
151
 
124
152
  # @!visibility private
@@ -129,7 +157,7 @@ module RunLoop
129
157
  # processes.
130
158
  # @return [Array<Integer>] An array of integer pids for instruments
131
159
  # processes. Returns an empty list if no instruments process are found.
132
- def pids_from_ps_output(ps_cmd=FIND_PIDS_CMD)
160
+ def pids_from_ps_output(ps_cmd=INSTRUMENTS_FIND_PIDS_CMD)
133
161
  ps_output = ps_for_instruments(ps_cmd)
134
162
  lines = ps_output.lines("\n").map { |line| line.strip }
135
163
  lines.map do |line|
@@ -166,39 +194,5 @@ module RunLoop
166
194
  def kill_signal(xcode_tools = RunLoop::XCTools.new)
167
195
  xcode_tools.xcode_version_gte_6? ? 'QUIT' : 'TERM'
168
196
  end
169
-
170
- # @!visibility private
171
- # Wait for Unix process with id `pid` to terminate.
172
- #
173
- # @param [Integer] pid The id of the process we are waiting on.
174
- # @param [Hash] options Values to control the behavior of this method.
175
- # @option options [Float] :timeout (2.0) How long to wait for the process to
176
- # terminate.
177
- # @option options [Float] :interval (0.1) The polling interval.
178
- # @option options [Boolean] :raise_on_no_terminate (false) Should an error
179
- # be raised if process does not terminate.
180
- # @raise [RuntimeError] If process does not terminate and
181
- # options[:raise_on_no_terminate] is truthy.
182
- def wait_for_process_to_terminate(pid, options={})
183
- default_opts = {:timeout => 2.0,
184
- :interval => 0.1,
185
- :raise_on_no_terminate => false}
186
- merged_opts = default_opts.merge(options)
187
-
188
- cmd = "ps #{pid} -o pid | grep #{pid}"
189
- poll_until = Time.now + merged_opts[:timeout]
190
- delay = merged_opts[:interval]
191
- has_terminated = false
192
- while Time.now < poll_until
193
- has_terminated = `#{cmd}`.strip == ''
194
- break if has_terminated
195
- sleep delay
196
- end
197
-
198
- if merged_opts[:raise_on_no_terminate] and not has_terminated
199
- details = `ps -p #{pid} -o pid,comm | grep #{pid}`.strip
200
- raise RuntimeError, "Waited #{merged_opts[:timeout]} s for process '#{details}' to terminate"
201
- end
202
- end
203
197
  end
204
198
  end
@@ -0,0 +1,55 @@
1
+ module RunLoop
2
+
3
+ # A class for interacting with the lldb command-line tool
4
+ class LLDB
5
+
6
+ # Returns a list of lldb pids.
7
+ # @return [Array<Integer>] An array of integer pids.
8
+ def self.lldb_pids
9
+ ps_output = `#{LLDB_FIND_PIDS_CMD}`.strip
10
+ lines = ps_output.lines("\n").map { |line| line.strip }
11
+ lldb_processes = lines.select { |line| self.is_lldb_process?(line) }
12
+ lldb_processes.map do |ps_description|
13
+ tokens = ps_description.strip.split(' ').map { |token| token.strip }
14
+ pid = tokens.fetch(0, nil)
15
+ if pid.nil?
16
+ nil
17
+ else
18
+ pid.to_i
19
+ end
20
+ end.compact.sort
21
+ end
22
+
23
+ # @!visibility private
24
+ # Is the process described an lldb process?
25
+ #
26
+ # @param [String] ps_details Details about a process as returned by `ps`
27
+ # @return [Boolean] True if the details describe an lldb process.
28
+ def self.is_lldb_process?(ps_details)
29
+ return false if ps_details.nil?
30
+ ps_details[/Contents\/Developer\/usr\/bin\/lldb/, 0] != nil
31
+ end
32
+
33
+ # Attempts to gracefully kill all running lldb processes.
34
+ def self.kill_lldb_processes
35
+ self.lldb_pids.each do |pid|
36
+ unless self.kill_with_signal(pid, 'TERM')
37
+ unless self.kill_with_signal(pid, 'QUIT')
38
+ self.kill_with_signal(pid, 'KILL')
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # @!visibility private
47
+ LLDB_FIND_PIDS_CMD = 'ps x -o pid,command | grep -v grep | grep lldb'
48
+
49
+ # @!visibility private
50
+ def self.kill_with_signal(pid, signal)
51
+ RunLoop::ProcessTerminator.new(pid, signal, 'lldb').kill_process
52
+ end
53
+
54
+ end
55
+ end
@@ -68,6 +68,23 @@ module RunLoop
68
68
  res == ''
69
69
  end
70
70
 
71
+ # Creates an new empty plist at `path`.
72
+ #
73
+ # Is not responsible for creating directories or ensuring write permissions.
74
+ #
75
+ # @param [String] path Where to create the new plist.
76
+ def create_plist(path)
77
+ File.open(path, 'w') do |file|
78
+ file.puts "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
79
+ file.puts "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
80
+ file.puts "<plist version=\"1.0\">"
81
+ file.puts '<dict>'
82
+ file.puts '</dict>'
83
+ file.puts '</plist>'
84
+ end
85
+ path
86
+ end
87
+
71
88
  private
72
89
 
73
90
  # returns the path to the PlistBuddy executable
@@ -168,6 +185,5 @@ module RunLoop
168
185
 
169
186
  "#{plist_buddy} -c #{cmd_part} \"#{file}\""
170
187
  end
171
-
172
188
  end
173
- end
189
+ end
@@ -0,0 +1,140 @@
1
+ module RunLoop
2
+
3
+ # A class for terminating processes and waiting for them to die.
4
+ class ProcessTerminator
5
+
6
+ # @!attribute [r] pid
7
+ # The process id of the process.
8
+ # @return [Integer] The pid.
9
+ attr_reader :pid
10
+
11
+ # @!attribute [r] kill_signal
12
+ # The kill signal to send to the process. Can be a Unix signal name or an
13
+ # Integer.
14
+ # @return [Integer, String] The kill signal.
15
+ attr_reader :kill_signal
16
+
17
+ # @!attribute [r] display_name
18
+ # The process name to use log messages and exceptions. Not used to find
19
+ # or otherwise interact with the process.
20
+ # @return [String] The display name.
21
+ attr_reader :display_name
22
+
23
+ # @!attribute [r] options
24
+ # Options to control the behavior of `kill_process`.
25
+ # @return [Hash] A hash of options.
26
+ attr_reader :options
27
+
28
+ # Create a new process terminator.
29
+ #
30
+ # @param[String,Integer] pid The process pid.
31
+ # @param[String, Integer] kill_signal The kill signal to send to the process.
32
+ # @param[String] display_name The name of the process to kill. Used only
33
+ # in log messages and exceptions.
34
+ # @option options [Float] :timeout (2.0) How long to wait for the process to
35
+ # terminate.
36
+ # @option options [Float] :interval (0.1) The polling interval.
37
+ # @option options [Boolean] :raise_on_no_terminate (false) Should an error
38
+ # be raised if process does not terminate.
39
+ def initialize(pid, kill_signal, display_name, options={})
40
+ @options = DEFAULT_OPTIONS.merge(options)
41
+ @pid = pid.to_i
42
+ @kill_signal = kill_signal
43
+ @display_name = display_name
44
+ end
45
+
46
+ # Try to kill the process identified by `pid`.
47
+ #
48
+ # After sending `kill_signal` to `pid`, wait for the process to terminate.
49
+ #
50
+ # @return [Boolean] Returns true if the process was terminated or is no
51
+ # longer alive.
52
+ # @raise [SignalException] Raised on an unhandled `Process.kill` exception.
53
+ # Errno:ESRCH and Errno:EPERM are _handled_ exceptions; all others will
54
+ # be raised.
55
+ def kill_process
56
+ return true unless process_alive?
57
+
58
+ debug_logging = RunLoop::Environment.debug?
59
+ begin
60
+ if debug_logging
61
+ puts "Sending '#{kill_signal}' to #{display_name} process '#{pid}'"
62
+ end
63
+ Process.kill(kill_signal, pid.to_i)
64
+ # Don't wait.
65
+ # We might not own this process and a WNOHANG would be a nop.
66
+ # Process.wait(pid, Process::WNOHANG)
67
+ rescue Errno::ESRCH
68
+ if debug_logging
69
+ puts "Process with pid '#{pid}' does not exist; nothing to do."
70
+ end
71
+ # Return early; there is no need to wait if the process does not exist.
72
+ return true
73
+ rescue Errno::EPERM
74
+ if debug_logging
75
+ puts "Cannot kill process '#{pid}' with '#{kill_signal}'; not a child of this process"
76
+ end
77
+ rescue SignalException => e
78
+ raise e.message
79
+ end
80
+
81
+ if debug_logging
82
+ puts "Waiting for #{display_name} '#{pid}' to terminate"
83
+ end
84
+ wait_for_process_to_terminate
85
+ end
86
+
87
+ # Is the process `pid` alive?
88
+ # @return [Boolean] Returns true if the process is still alive.
89
+ def process_alive?
90
+ begin
91
+ Process.kill(0, pid.to_i)
92
+ true
93
+ rescue Errno::ESRCH
94
+ false
95
+ rescue Errno::EPERM
96
+ true
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ # @!visibility private
103
+ # The default options for waiting on a process to terminate.
104
+ DEFAULT_OPTIONS =
105
+ {
106
+ :timeout => 2.0,
107
+ :interval => 0.1,
108
+ :raise_on_no_terminate => false
109
+ }
110
+
111
+ # @!visibility private
112
+ # The details of the process reported by `ps`.
113
+ def ps_details
114
+ `xcrun ps -p #{pid} -o pid,comm | grep #{pid}`.strip
115
+ end
116
+
117
+ # @!visibility private
118
+ # Wait for the process to terminate by polling.
119
+ def wait_for_process_to_terminate
120
+ now = Time.now
121
+ poll_until = now + options[:timeout]
122
+ delay = options[:interval]
123
+ has_terminated = false
124
+ while Time.now < poll_until
125
+ has_terminated = !process_alive?
126
+ break if has_terminated
127
+ sleep delay
128
+ end
129
+
130
+ if RunLoop::Environment.debug?
131
+ puts "Waited for #{Time.now - now} seconds for #{display_name} with '#{pid}' to terminate"
132
+ end
133
+
134
+ if @options[:raise_on_no_terminate] and !has_terminated
135
+ raise "Waited #{options[:timeout]} seconds for #{display_name} (#{ps_details}) to terminate"
136
+ end
137
+ has_terminated
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,85 @@
1
+ module RunLoop
2
+
3
+ # A class for waiting on processes.
4
+ class ProcessWaiter
5
+
6
+ attr_reader :process_name
7
+
8
+ def initialize(process_name, options={})
9
+ @options = DEFAULT_OPTIONS.merge(options)
10
+ @process_name = process_name
11
+ end
12
+
13
+ # Collect a list of Integer pids.
14
+ # @return [Array<Integer>] An array of integer pids for the `process_name`
15
+ def pids
16
+ process_info = `ps x -o pid,comm | grep -v grep | grep #{process_name}`
17
+ process_array = process_info.split("\n")
18
+ process_array.map { |process| process.split(' ').first.strip.to_i }
19
+ end
20
+
21
+ # Is the `process_name` a running?
22
+ def running_process?
23
+ !pids.empty?
24
+ end
25
+
26
+ # Wait for `process_name` to start.
27
+ def wait_for_any
28
+ return true if running_process?
29
+
30
+ now = Time.now
31
+ poll_until = now + @options[:timeout]
32
+ delay = @options[:interval]
33
+ is_alive = false
34
+ while Time.now < poll_until
35
+ is_alive = running_process?
36
+ break if is_alive
37
+ sleep delay
38
+ end
39
+
40
+ if RunLoop::Environment.debug?
41
+ puts "Waited for #{Time.now - now} seconds for '#{process_name}' to start."
42
+ end
43
+
44
+ if @options[:raise_on_timeout] and !is_alive
45
+ raise "Waited #{@options[:timeout]} seconds for '#{process_name}' to start."
46
+ end
47
+ is_alive
48
+ end
49
+
50
+ # Wait for all `process_name` to finish.
51
+ def wait_for_none
52
+ return true if !running_process?
53
+
54
+ now = Time.now
55
+ poll_until = now + @options[:timeout]
56
+ delay = @options[:interval]
57
+ has_terminated = false
58
+ while Time.now < poll_until
59
+ has_terminated = !self.running_process?
60
+ break if has_terminated
61
+ sleep delay
62
+ end
63
+
64
+ if RunLoop::Environment.debug?
65
+ puts "Waited for #{Time.now - now} seconds for '#{process_name}' to die."
66
+ end
67
+
68
+ if @options[:raise_on_timeout] and !has_terminated
69
+ raise "Waited #{@options[:timeout]} seconds for '#{process_name}' to die."
70
+ end
71
+ has_terminated
72
+ end
73
+
74
+ private
75
+
76
+ # @!visibility private
77
+ DEFAULT_OPTIONS =
78
+ {
79
+ :timeout => 10.0,
80
+ :interval => 0.1,
81
+ :raise_on_timeout => false
82
+ }
83
+ end
84
+ end
85
+